Back to Blog
January 22, 2024
Next.js AI Team
16 min read
Next.js & Vercel

Advanced Next.js AI Patterns: Server Components, Edge & Real-Time with Zapserp

Master advanced Next.js patterns for AI applications with Zapserp. Learn server components, edge functions, real-time updates, and enterprise deployment strategies.

nextjsserver-componentsedge-functionsreal-timewebsocketsenterpriseai-patterns

Advanced Next.js AI Patterns: Server Components, Edge & Real-Time with Zapserp

Building production-grade AI applications requires mastering advanced Next.js patterns. This comprehensive guide explores server components, edge functions, real-time updates, and enterprise deployment strategies when integrating Zapserp for intelligent web data retrieval.

We'll cover cutting-edge patterns that leverage Next.js 14+ features for maximum performance, scalability, and user experience.

Server Components for AI Data Fetching

Static AI Content Generation

Server components excel at pre-generating AI content during build time or server-side rendering:

// app/research/[topic]/page.tsx
import { Zapserp, SearchEngine } from 'zapserp'
import { Suspense } from 'react'
import { ResearchSummary } from '@/components/research-summary'
import { SourcesList } from '@/components/sources-list'

interface PageProps {
  params: { topic: string }
  searchParams: { depth?: string; sources?: string }
}

// This runs on the server during build/request time
async function generateResearchData(topic: string, options: any = {}) {
  const zapserp = new Zapserp({ apiKey: process.env.ZAPSERP_API_KEY! })
  
  try {
    // Generate multiple search queries for comprehensive coverage
    const searchQueries = [
      `${topic} overview latest`,
      `${topic} research developments 2024`,
      `${topic} expert analysis recent`,
      `${topic} trends insights current`
    ]
    
    const allResults = []
    
    for (const query of searchQueries) {
      const searchResponse = await zapserp.search({
        query,
        engines: [SearchEngine.GOOGLE, SearchEngine.BING],
        limit: 5,
        language: 'en'
      })
      
      if (searchResponse.results.length > 0) {
        const urls = searchResponse.results.map(r => r.url).slice(0, 3)
        const contentResponse = await zapserp.readerBatch({ urls })
        
        allResults.push(...contentResponse.results.filter(r => r && r.content))
      }
    }
    
    return {
      topic,
      sources: allResults,
      generatedAt: new Date().toISOString(),
      totalSources: allResults.length
    }
    
  } catch (error) {
    console.error('Research generation failed:', error)
    return null
  }
}

// Server Component - renders on server
export default async function ResearchPage({ params, searchParams }: PageProps) {
  const { topic } = params
  const decodedTopic = decodeURIComponent(topic)
  
  const researchData = await generateResearchData(decodedTopic, {
    depth: searchParams.depth || 'standard',
    sources: searchParams.sources || 'web'
  })
  
  if (!researchData) {
    return (
      <div className="container mx-auto py-8">
        <div className="text-center">
          <h1 className="text-2xl font-bold mb-4">Research Failed</h1>
          <p className="text-muted-foreground">
            Unable to generate research for "{decodedTopic}". Please try again.
          </p>
        </div>
      </div>
    )
  }
  
  return (
    <div className="container mx-auto py-8 space-y-8">
      <div className="text-center">
        <h1 className="text-4xl font-bold mb-4">
          Research: {decodedTopic}
        </h1>
        <p className="text-muted-foreground">
          Generated from {researchData.totalSources} sources • {researchData.generatedAt}
        </p>
      </div>
      
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        <div className="lg:col-span-2">
          <Suspense fallback={<ResearchSkeleton />}>
            <ResearchSummary 
              topic={decodedTopic}
              sources={researchData.sources}
            />
          </Suspense>
        </div>
        
        <div>
          <Suspense fallback={<SourcesSkeleton />}>
            <SourcesList sources={researchData.sources} />
          </Suspense>
        </div>
      </div>
    </div>
  )
}

// Generate static params for popular topics
export async function generateStaticParams() {
  const popularTopics = [
    'artificial-intelligence',
    'climate-change',
    'space-exploration',
    'quantum-computing',
    'renewable-energy',
    'biotechnology'
  ]
  
  return popularTopics.map((topic) => ({
    topic
  }))
}

// Skeletons for loading states
function ResearchSkeleton() {
  return (
    <div className="space-y-4">
      <div className="h-8 bg-muted rounded animate-pulse" />
      <div className="space-y-2">
        <div className="h-4 bg-muted rounded animate-pulse" />
        <div className="h-4 bg-muted rounded animate-pulse w-3/4" />
        <div className="h-4 bg-muted rounded animate-pulse w-1/2" />
      </div>
    </div>
  )
}

function SourcesSkeleton() {
  return (
    <div className="space-y-3">
      {Array.from({ length: 5 }).map((_, i) => (
        <div key={i} className="p-3 border rounded">
          <div className="h-4 bg-muted rounded animate-pulse mb-2" />
          <div className="h-3 bg-muted rounded animate-pulse w-2/3" />
        </div>
      ))}
    </div>
  )
}

Dynamic Server Components with Caching

Implement intelligent caching for server components:

// lib/server-cache.ts
import { unstable_cache } from 'next/cache'
import { searchWithZapserp } from '@/lib/zapserp'

// Cached search function with Next.js Data Cache
export const getCachedSearchResults = unstable_cache(
  async (query: string, options: any = {}) => {
    console.log('Cache miss - performing fresh search:', query)
    return await searchWithZapserp(query, options)
  },
  ['search-results'],
  {
    revalidate: 300, // 5 minutes
    tags: ['search', 'zapserp']
  }
)

// Cached AI analysis
export const getCachedAIAnalysis = unstable_cache(
  async (content: string, analysisType: string) => {
    const response = await fetch('/api/analyze', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content, analysisType })
    })
    
    if (!response.ok) throw new Error('Analysis failed')
    return response.json()
  },
  ['ai-analysis'],
  {
    revalidate: 600, // 10 minutes
    tags: ['analysis', 'ai']
  }
)

// Server component using cached data
export async function TrendingTopics({ category }: { category: string }) {
  const searchResults = await getCachedSearchResults(
    `${category} trending topics 2024`,
    { limit: 10, includeContent: true }
  )
  
  const analysis = await getCachedAIAnalysis(
    JSON.stringify(searchResults),
    'trending-analysis'
  )
  
  return (
    <div className="space-y-4">
      <h2 className="text-2xl font-bold">Trending in {category}</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
        {analysis.trends.map((trend: any, index: number) => (
          <TrendCard key={index} trend={trend} />
        ))}
      </div>
    </div>
  )
}

Edge Functions for Global Performance

Intelligent Edge Routing

Implement geo-aware search routing based on user location:

// app/api/search/edge/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { Zapserp, SearchEngine } from 'zapserp'

export const runtime = 'edge'

const zapserp = new Zapserp({ apiKey: process.env.ZAPSERP_API_KEY! })

// Regional search configurations
const REGIONAL_CONFIGS = {
  'US': {
    engines: [SearchEngine.GOOGLE, SearchEngine.BING],
    country: 'us',
    language: 'en'
  },
  'EU': {
    engines: [SearchEngine.GOOGLE, SearchEngine.BING],
    country: 'gb',
    language: 'en'
  },
  'ASIA': {
    engines: [SearchEngine.GOOGLE],
    country: 'sg',
    language: 'en'
  }
}

export async function POST(request: NextRequest) {
  try {
    const { query, options = {} } = await request.json()
    
    // Detect user region from Vercel headers
    const country = request.geo?.country || 'US'
    const region = getRegionFromCountry(country)
    const config = REGIONAL_CONFIGS[region] || REGIONAL_CONFIGS['US']
    
    // Perform region-optimized search
    const searchResponse = await zapserp.search({
      query,
      engines: config.engines,
      country: config.country,
      language: config.language,
      limit: options.limit || 10,
      ...options
    })
    
    // Add metadata about the search
    const response = {
      results: searchResponse.results,
      metadata: {
        region,
        country,
        searchTime: new Date().toISOString(),
        engines: config.engines,
        totalResults: searchResponse.results.length
      }
    }
    
    return NextResponse.json(response, {
      headers: {
        'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
        'X-Search-Region': region,
        'X-Search-Country': country
      }
    })
    
  } catch (error) {
    console.error('Edge search failed:', error)
    return NextResponse.json(
      { error: 'Search failed', message: error.message },
      { status: 500 }
    )
  }
}

function getRegionFromCountry(country: string): keyof typeof REGIONAL_CONFIGS {
  const countryToRegion: Record<string, keyof typeof REGIONAL_CONFIGS> = {
    'US': 'US', 'CA': 'US', 'MX': 'US',
    'GB': 'EU', 'DE': 'EU', 'FR': 'EU', 'IT': 'EU', 'ES': 'EU',
    'SG': 'ASIA', 'JP': 'ASIA', 'KR': 'ASIA', 'AU': 'ASIA'
  }
  
  return countryToRegion[country] || 'US'
}

Edge-Optimized Content Extraction

Create an edge function for fast content extraction:

// app/api/extract/edge/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { Zapserp } from 'zapserp'

export const runtime = 'edge'

const zapserp = new Zapserp({ apiKey: process.env.ZAPSERP_API_KEY! })

interface ExtractionRequest {
  urls: string[]
  options?: {
    summary?: boolean
    maxLength?: number
    includeMetadata?: boolean
  }
}

export async function POST(request: NextRequest) {
  try {
    const { urls, options = {} }: ExtractionRequest = await request.json()
    
    if (!urls || urls.length === 0) {
      return NextResponse.json(
        { error: 'URLs are required' },
        { status: 400 }
      )
    }
    
    if (urls.length > 10) {
      return NextResponse.json(
        { error: 'Maximum 10 URLs allowed' },
        { status: 400 }
      )
    }
    
    // Extract content from URLs
    const contentResponse = await zapserp.readerBatch({ urls })
    
    // Process results based on options
    const processedResults = contentResponse.results.map(result => {
      if (!result) return null
      
      let processedContent = result.content
      
      // Apply content length limit
      if (options.maxLength && processedContent.length > options.maxLength) {
        processedContent = processedContent.substring(0, options.maxLength) + '...'
      }
      
      const response: any = {
        url: result.url,
        title: result.title,
        content: processedContent,
        contentLength: result.contentLength
      }
      
      // Include metadata if requested
      if (options.includeMetadata) {
        response.metadata = result.metadata
      }
      
      // Generate summary if requested (simplified)
      if (options.summary) {
        response.summary = generateQuickSummary(processedContent)
      }
      
      return response
    }).filter(Boolean)
    
    return NextResponse.json({
      results: processedResults,
      metadata: {
        totalUrls: urls.length,
        successfulExtractions: processedResults.length,
        extractedAt: new Date().toISOString()
      }
    }, {
      headers: {
        'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
      }
    })
    
  } catch (error) {
    console.error('Edge extraction failed:', error)
    return NextResponse.json(
      { error: 'Extraction failed', message: error.message },
      { status: 500 }
    )
  }
}

function generateQuickSummary(content: string): string {
  // Simple summary generation (first meaningful paragraph)
  const paragraphs = content
    .split('\n')
    .map(p => p.trim())
    .filter(p => p.length > 50)
  
  if (paragraphs.length > 0) {
    const firstParagraph = paragraphs[0]
    return firstParagraph.length > 200 
      ? firstParagraph.substring(0, 200) + '...'
      : firstParagraph
  }
  
  return content.substring(0, 150) + '...'
}

Real-Time Features with WebSockets

Real-Time Search Collaboration

Implement collaborative search sessions where multiple users can see searches in real-time:

// lib/websocket-server.ts
import { WebSocket, WebSocketServer } from 'ws'
import { IncomingMessage } from 'http'

interface SearchSession {
  id: string
  participants: Set<WebSocket>
  lastActivity: Date
  searchHistory: SearchEvent[]
}

interface SearchEvent {
  id: string
  query: string
  results: any[]
  timestamp: Date
  userId: string
}

class CollaborativeSearchServer {
  private wss: WebSocketServer
  private sessions = new Map<string, SearchSession>()
  
  constructor(port: number) {
    this.wss = new WebSocketServer({ port })
    this.setupWebSocketHandlers()
    this.startCleanupInterval()
  }
  
  private setupWebSocketHandlers() {
    this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
      console.log('New WebSocket connection')
      
      ws.on('message', async (data) => {
        try {
          const message = JSON.parse(data.toString())
          await this.handleMessage(ws, message)
        } catch (error) {
          console.error('WebSocket message error:', error)
          ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }))
        }
      })
      
      ws.on('close', () => {
        this.removeFromAllSessions(ws)
      })
    })
  }
  
  private async handleMessage(ws: WebSocket, message: any) {
    switch (message.type) {
      case 'join_session':
        this.joinSession(ws, message.sessionId, message.userId)
        break
        
      case 'search':
        await this.handleSearchMessage(ws, message)
        break
        
      case 'leave_session':
        this.leaveSession(ws, message.sessionId)
        break
    }
  }
  
  private joinSession(ws: WebSocket, sessionId: string, userId: string) {
    let session = this.sessions.get(sessionId)
    
    if (!session) {
      session = {
        id: sessionId,
        participants: new Set(),
        lastActivity: new Date(),
        searchHistory: []
      }
      this.sessions.set(sessionId, session)
    }
    
    session.participants.add(ws)
    session.lastActivity = new Date()
    
    // Add session info to WebSocket
    ;(ws as any).sessionId = sessionId
    ;(ws as any).userId = userId
    
    // Send session info to new participant
    ws.send(JSON.stringify({
      type: 'session_joined',
      sessionId,
      participantCount: session.participants.size,
      searchHistory: session.searchHistory.slice(-10) // Last 10 searches
    }))
    
    // Notify other participants
    this.broadcastToSession(sessionId, {
      type: 'participant_joined',
      userId,
      participantCount: session.participants.size
    }, ws)
  }
  
  private async handleSearchMessage(ws: WebSocket, message: any) {
    const sessionId = (ws as any).sessionId
    const userId = (ws as any).userId
    
    if (!sessionId) {
      ws.send(JSON.stringify({ type: 'error', message: 'Not in a session' }))
      return
    }
    
    try {
      // Perform search using Zapserp
      const zapserp = new Zapserp({ apiKey: process.env.ZAPSERP_API_KEY! })
      const searchResponse = await zapserp.search({
        query: message.query,
        limit: message.limit || 5
      })
      
      const searchEvent: SearchEvent = {
        id: generateId(),
        query: message.query,
        results: searchResponse.results,
        timestamp: new Date(),
        userId
      }
      
      // Add to session history
      const session = this.sessions.get(sessionId)
      if (session) {
        session.searchHistory.push(searchEvent)
        session.lastActivity = new Date()
        
        // Keep only last 50 searches
        if (session.searchHistory.length > 50) {
          session.searchHistory = session.searchHistory.slice(-50)
        }
      }
      
      // Broadcast search to all participants
      this.broadcastToSession(sessionId, {
        type: 'search_result',
        searchEvent
      })
      
    } catch (error) {
      console.error('Search failed:', error)
      ws.send(JSON.stringify({
        type: 'search_error',
        message: 'Search failed',
        query: message.query
      }))
    }
  }
  
  private broadcastToSession(sessionId: string, message: any, except?: WebSocket) {
    const session = this.sessions.get(sessionId)
    if (!session) return
    
    const messageStr = JSON.stringify(message)
    
    session.participants.forEach(ws => {
      if (ws !== except && ws.readyState === WebSocket.OPEN) {
        ws.send(messageStr)
      }
    })
  }
  
  private leaveSession(ws: WebSocket, sessionId: string) {
    const session = this.sessions.get(sessionId)
    if (!session) return
    
    session.participants.delete(ws)
    
    if (session.participants.size === 0) {
      this.sessions.delete(sessionId)
    } else {
      this.broadcastToSession(sessionId, {
        type: 'participant_left',
        userId: (ws as any).userId,
        participantCount: session.participants.size
      })
    }
  }
  
  private removeFromAllSessions(ws: WebSocket) {
    for (const [sessionId, session] of this.sessions.entries()) {
      if (session.participants.has(ws)) {
        this.leaveSession(ws, sessionId)
        break
      }
    }
  }
  
  private startCleanupInterval() {
    setInterval(() => {
      const now = new Date()
      const maxAge = 60 * 60 * 1000 // 1 hour
      
      for (const [sessionId, session] of this.sessions.entries()) {
        if (now.getTime() - session.lastActivity.getTime() > maxAge) {
          this.sessions.delete(sessionId)
        }
      }
    }, 5 * 60 * 1000) // Every 5 minutes
  }
}

function generateId(): string {
  return Math.random().toString(36).substring(2) + Date.now().toString(36)
}

// Start WebSocket server
export function startCollaborativeSearchServer(port: number = 3001) {
  return new CollaborativeSearchServer(port)
}

Real-Time Client Component

Create a React component for real-time collaborative search:

// components/collaborative-search.tsx
'use client'

import React, { useState, useEffect, useRef } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Users, Search, Send, History } from 'lucide-react'

interface SearchEvent {
  id: string
  query: string
  results: any[]
  timestamp: string
  userId: string
}

interface CollaborativeSearchProps {
  sessionId: string
  userId: string
  websocketUrl?: string
}

export function CollaborativeSearch({ 
  sessionId, 
  userId, 
  websocketUrl = 'ws://localhost:3001' 
}: CollaborativeSearchProps) {
  const [connected, setConnected] = useState(false)
  const [participantCount, setParticipantCount] = useState(0)
  const [searchHistory, setSearchHistory] = useState<SearchEvent[]>([])
  const [currentSearch, setCurrentSearch] = useState('')
  const [isSearching, setIsSearching] = useState(false)
  const wsRef = useRef<WebSocket | null>(null)
  
  useEffect(() => {
    connectWebSocket()
    
    return () => {
      if (wsRef.current) {
        wsRef.current.close()
      }
    }
  }, [sessionId, userId])
  
  const connectWebSocket = () => {
    try {
      const ws = new WebSocket(websocketUrl)
      wsRef.current = ws
      
      ws.onopen = () => {
        console.log('WebSocket connected')
        setConnected(true)
        
        // Join the session
        ws.send(JSON.stringify({
          type: 'join_session',
          sessionId,
          userId
        }))
      }
      
      ws.onmessage = (event) => {
        const message = JSON.parse(event.data)
        handleWebSocketMessage(message)
      }
      
      ws.onclose = () => {
        console.log('WebSocket disconnected')
        setConnected(false)
        
        // Attempt to reconnect after delay
        setTimeout(() => {
          if (wsRef.current?.readyState === WebSocket.CLOSED) {
            connectWebSocket()
          }
        }, 3000)
      }
      
      ws.onerror = (error) => {
        console.error('WebSocket error:', error)
        setConnected(false)
      }
      
    } catch (error) {
      console.error('Failed to connect WebSocket:', error)
    }
  }
  
  const handleWebSocketMessage = (message: any) => {
    switch (message.type) {
      case 'session_joined':
        setParticipantCount(message.participantCount)
        setSearchHistory(message.searchHistory || [])
        break
        
      case 'participant_joined':
      case 'participant_left':
        setParticipantCount(message.participantCount)
        break
        
      case 'search_result':
        setSearchHistory(prev => [...prev, message.searchEvent])
        setIsSearching(false)
        break
        
      case 'search_error':
        setIsSearching(false)
        console.error('Search error:', message.message)
        break
    }
  }
  
  const handleSearch = () => {
    if (!currentSearch.trim() || !connected || isSearching) return
    
    setIsSearching(true)
    
    wsRef.current?.send(JSON.stringify({
      type: 'search',
      query: currentSearch.trim(),
      limit: 5
    }))
    
    setCurrentSearch('')
  }
  
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      handleSearch()
    }
  }
  
  return (
    <div className="space-y-6">
      {/* Session Header */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center justify-between">
            <span>Collaborative Search Session</span>
            <div className="flex items-center gap-2">
              <Badge variant={connected ? "default" : "destructive"}>
                {connected ? 'Connected' : 'Disconnected'}
              </Badge>
              <Badge variant="secondary">
                <Users className="w-3 h-3 mr-1" />
                {participantCount} participants
              </Badge>
            </div>
          </CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex gap-2">
            <Input
              value={currentSearch}
              onChange={(e) => setCurrentSearch(e.target.value)}
              onKeyPress={handleKeyPress}
              placeholder="Search collaboratively..."
              disabled={!connected || isSearching}
            />
            <Button 
              onClick={handleSearch}
              disabled={!connected || isSearching || !currentSearch.trim()}
            >
              {isSearching ? (
                <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
              ) : (
                <Send className="w-4 h-4" />
              )}
            </Button>
          </div>
        </CardContent>
      </Card>
      
      {/* Search History */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <History className="w-5 h-5" />
            Search History
          </CardTitle>
        </CardHeader>
        <CardContent>
          <div className="space-y-4 max-h-96 overflow-y-auto">
            {searchHistory.length === 0 ? (
              <p className="text-center text-muted-foreground py-8">
                No searches yet. Start searching to collaborate!
              </p>
            ) : (
              searchHistory.map((search) => (
                <SearchResultCard key={search.id} searchEvent={search} />
              ))
            )}
          </div>
        </CardContent>
      </Card>
    </div>
  )
}

function SearchResultCard({ searchEvent }: { searchEvent: SearchEvent }) {
  return (
    <div className="border rounded-lg p-4 space-y-3">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <Search className="w-4 h-4" />
          <span className="font-medium">{searchEvent.query}</span>
        </div>
        <div className="text-xs text-muted-foreground">
          {searchEvent.userId} • {new Date(searchEvent.timestamp).toLocaleTimeString()}
        </div>
      </div>
      
      <div className="space-y-2">
        {searchEvent.results.slice(0, 3).map((result, index) => (
          <div key={index} className="text-sm">
            <a 
              href={result.url} 
              target="_blank" 
              rel="noopener noreferrer"
              className="text-blue-600 hover:underline font-medium"
            >
              {result.title}
            </a>
            <p className="text-muted-foreground text-xs mt-1">
              {result.snippet}
            </p>
          </div>
        ))}
      </div>
      
      <Badge variant="outline" className="text-xs">
        {searchEvent.results.length} results
      </Badge>
    </div>
  )
}

Enterprise Deployment Patterns

Multi-Tenant Architecture

Implement a multi-tenant system for enterprise deployments:

// lib/tenant-manager.ts
interface TenantConfig {
  id: string
  name: string
  apiQuota: {
    daily: number
    hourly: number
  }
  features: {
    advancedSearch: boolean
    realTimeFeatures: boolean
    customModels: boolean
  }
  searchEngines: string[]
  customPrompts?: Record<string, string>
}

class TenantManager {
  private tenants = new Map<string, TenantConfig>()
  private usage = new Map<string, { daily: number, hourly: number, lastReset: Date }>()
  
  constructor() {
    this.loadTenantsFromDatabase()
    this.startUsageResetInterval()
  }
  
  async getTenant(tenantId: string): Promise<TenantConfig | null> {
    return this.tenants.get(tenantId) || null
  }
  
  async checkQuota(tenantId: string): Promise<{ allowed: boolean, remaining: { daily: number, hourly: number } }> {
    const tenant = await this.getTenant(tenantId)
    if (!tenant) return { allowed: false, remaining: { daily: 0, hourly: 0 } }
    
    const usage = this.getUsage(tenantId)
    
    const dailyRemaining = Math.max(0, tenant.apiQuota.daily - usage.daily)
    const hourlyRemaining = Math.max(0, tenant.apiQuota.hourly - usage.hourly)
    
    return {
      allowed: dailyRemaining > 0 && hourlyRemaining > 0,
      remaining: {
        daily: dailyRemaining,
        hourly: hourlyRemaining
      }
    }
  }
  
  async incrementUsage(tenantId: string): Promise<void> {
    const usage = this.getUsage(tenantId)
    usage.daily++
    usage.hourly++
  }
  
  private getUsage(tenantId: string) {
    let usage = this.usage.get(tenantId)
    
    if (!usage) {
      usage = {
        daily: 0,
        hourly: 0,
        lastReset: new Date()
      }
      this.usage.set(tenantId, usage)
    }
    
    return usage
  }
  
  private async loadTenantsFromDatabase() {
    // Load tenant configurations from your database
    // This is a simplified implementation
    const sampleTenants: TenantConfig[] = [
      {
        id: 'enterprise-1',
        name: 'Enterprise Corp',
        apiQuota: { daily: 10000, hourly: 1000 },
        features: { advancedSearch: true, realTimeFeatures: true, customModels: true },
        searchEngines: ['google', 'bing', 'duckduckgo']
      },
      {
        id: 'startup-1',
        name: 'Startup Inc',
        apiQuota: { daily: 1000, hourly: 100 },
        features: { advancedSearch: false, realTimeFeatures: true, customModels: false },
        searchEngines: ['google', 'bing']
      }
    ]
    
    sampleTenants.forEach(tenant => {
      this.tenants.set(tenant.id, tenant)
    })
  }
  
  private startUsageResetInterval() {
    // Reset hourly usage every hour
    setInterval(() => {
      for (const usage of this.usage.values()) {
        usage.hourly = 0
      }
    }, 60 * 60 * 1000)
    
    // Reset daily usage every day
    setInterval(() => {
      for (const usage of this.usage.values()) {
        usage.daily = 0
      }
    }, 24 * 60 * 60 * 1000)
  }
}

export const tenantManager = new TenantManager()

// Middleware for tenant validation
export async function validateTenant(request: Request, tenantId: string) {
  const tenant = await tenantManager.getTenant(tenantId)
  if (!tenant) {
    return { valid: false, error: 'Invalid tenant' }
  }
  
  const quota = await tenantManager.checkQuota(tenantId)
  if (!quota.allowed) {
    return { valid: false, error: 'Quota exceeded', quota }
  }
  
  return { valid: true, tenant, quota }
}

Enterprise API Route with Tenant Support

// app/api/enterprise/search/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { validateTenant, tenantManager } from '@/lib/tenant-manager'
import { searchWithZapserp } from '@/lib/zapserp'

export const runtime = 'edge'

export async function POST(request: NextRequest) {
  try {
    const { tenantId, query, options = {} } = await request.json()
    
    if (!tenantId || !query) {
      return NextResponse.json(
        { error: 'Tenant ID and query are required' },
        { status: 400 }
      )
    }
    
    // Validate tenant and check quota
    const validation = await validateTenant(request, tenantId)
    if (!validation.valid) {
      return NextResponse.json(
        { error: validation.error, quota: validation.quota },
        { status: validation.error === 'Quota exceeded' ? 429 : 403 }
      )
    }
    
    const { tenant } = validation
    
    // Increment usage before processing
    await tenantManager.incrementUsage(tenantId)
    
    // Apply tenant-specific search configuration
    const searchOptions = {
      ...options,
      engines: tenant.searchEngines,
      limit: Math.min(options.limit || 10, 50) // Limit results
    }
    
    // Perform search with tenant configuration
    const results = await searchWithZapserp(query, searchOptions)
    
    // Apply tenant-specific features
    let enhancedResults = results
    
    if (tenant.features.advancedSearch) {
      // Add advanced search features for enterprise tenants
      enhancedResults = await enhanceResultsForEnterprise(results)
    }
    
    return NextResponse.json({
      results: enhancedResults,
      metadata: {
        tenantId,
        searchTime: new Date().toISOString(),
        quotaRemaining: validation.quota?.remaining,
        features: tenant.features
      }
    }, {
      headers: {
        'X-Tenant-ID': tenantId,
        'X-Quota-Remaining-Daily': validation.quota?.remaining.daily.toString() || '0',
        'X-Quota-Remaining-Hourly': validation.quota?.remaining.hourly.toString() || '0'
      }
    })
    
  } catch (error) {
    console.error('Enterprise search failed:', error)
    return NextResponse.json(
      { error: 'Search failed', message: error.message },
      { status: 500 }
    )
  }
}

async function enhanceResultsForEnterprise(results: any[]): Promise<any[]> {
  // Add enterprise-specific enhancements
  return results.map(result => ({
    ...result,
    enhanced: true,
    confidence: calculateConfidenceScore(result),
    categories: categorizeResult(result)
  }))
}

function calculateConfidenceScore(result: any): number {
  // Simple confidence scoring based on various factors
  let score = 0.5
  
  if (result.title && result.title.length > 10) score += 0.1
  if (result.snippet && result.snippet.length > 50) score += 0.1
  if (result.content && result.content.length > 200) score += 0.2
  if (result.metadata?.author) score += 0.1
  
  return Math.min(score, 1.0)
}

function categorizeResult(result: any): string[] {
  const categories = []
  const content = (result.title + ' ' + result.snippet + ' ' + (result.content || '')).toLowerCase()
  
  if (content.includes('news') || content.includes('breaking')) categories.push('news')
  if (content.includes('research') || content.includes('study')) categories.push('research')
  if (content.includes('technology') || content.includes('tech')) categories.push('technology')
  if (content.includes('business') || content.includes('finance')) categories.push('business')
  
  return categories
}

Monitoring and Analytics

Performance Monitoring

Implement comprehensive monitoring for your Next.js AI application:

// lib/monitoring.ts
interface PerformanceMetric {
  operation: string
  duration: number
  success: boolean
  timestamp: Date
  metadata?: Record<string, any>
}

class PerformanceMonitor {
  private metrics: PerformanceMetric[] = []
  
  async measureOperation<T>(
    operation: string,
    asyncFn: () => Promise<T>,
    metadata?: Record<string, any>
  ): Promise<T> {
    const startTime = Date.now()
    let success = false
    let result: T
    
    try {
      result = await asyncFn()
      success = true
      return result
    } catch (error) {
      throw error
    } finally {
      const duration = Date.now() - startTime
      
      this.metrics.push({
        operation,
        duration,
        success,
        timestamp: new Date(),
        metadata
      })
      
      // Log slow operations
      if (duration > 5000) {
        console.warn(`Slow operation: ${operation} took ${duration}ms`)
      }
      
      // Keep metrics manageable
      if (this.metrics.length > 1000) {
        this.metrics = this.metrics.slice(-500)
      }
    }
  }
  
  getMetrics(timeframe: number = 3600000): any {
    const cutoff = Date.now() - timeframe
    const recentMetrics = this.metrics.filter(m => 
      m.timestamp.getTime() > cutoff
    )
    
    if (recentMetrics.length === 0) return null
    
    const successful = recentMetrics.filter(m => m.success)
    const failed = recentMetrics.filter(m => !m.success)
    
    return {
      total: recentMetrics.length,
      successful: successful.length,
      failed: failed.length,
      successRate: (successful.length / recentMetrics.length) * 100,
      averageDuration: successful.reduce((sum, m) => sum + m.duration, 0) / successful.length,
      slowestOperation: Math.max(...recentMetrics.map(m => m.duration)),
      fastestOperation: Math.min(...successful.map(m => m.duration))
    }
  }
}

export const performanceMonitor = new PerformanceMonitor()

// Usage in API routes
export async function monitoredSearch(query: string, options: any = {}) {
  return performanceMonitor.measureOperation(
    'zapserp_search',
    () => searchWithZapserp(query, options),
    { query, options }
  )
}

Key Benefits & Production Tips

Benefits of Advanced Patterns

  1. Server Components: Improved SEO and faster initial page loads
  2. Edge Functions: Global performance and reduced latency
  3. Real-Time Features: Enhanced user collaboration and engagement
  4. Multi-Tenant: Scalable enterprise deployment
  5. Monitoring: Production-ready observability and debugging

Production Deployment Tips

  1. Environment Variables: Secure API key management with Vercel
  2. Edge Optimization: Use edge runtime for globally distributed performance
  3. Caching Strategy: Implement intelligent caching for cost optimization
  4. Error Boundaries: Robust error handling for production reliability
  5. Monitoring: Real-time performance and usage monitoring

Next Steps

Ready to implement advanced Next.js AI patterns with Zapserp?

  1. Start with Server Components: Implement static content generation
  2. Add Edge Functions: Deploy globally distributed AI endpoints
  3. Implement Real-Time: Add collaborative features for user engagement
  4. Scale for Enterprise: Add multi-tenant support for B2B deployment
  5. Monitor Performance: Implement comprehensive monitoring and analytics

Need enterprise deployment guidance? Contact our team for architecture consultation and advanced integration support.

Found this helpful?

Share it with your network and help others discover great content.

Related Articles

Complete guide to building streaming AI applications with Next.js, Vercel AI SDK, and Zapserp. Learn to create real-time RAG systems, AI chatbots, and deploy to Vercel Edge.

14 min read
Next.js & Vercel

Build a live stock market monitoring dashboard using Zapserp for real-time financial news and market analysis. Complete with React components and WebSocket updates.

4 min read
Financial Apps

Quick tutorial to create a fully functional news aggregator using Zapserp. Get real-time news from multiple sources with just a few lines of code.

4 min read
Quick Start