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
- Server Components: Improved SEO and faster initial page loads
- Edge Functions: Global performance and reduced latency
- Real-Time Features: Enhanced user collaboration and engagement
- Multi-Tenant: Scalable enterprise deployment
- Monitoring: Production-ready observability and debugging
Production Deployment Tips
- Environment Variables: Secure API key management with Vercel
- Edge Optimization: Use edge runtime for globally distributed performance
- Caching Strategy: Implement intelligent caching for cost optimization
- Error Boundaries: Robust error handling for production reliability
- Monitoring: Real-time performance and usage monitoring
Next Steps
Ready to implement advanced Next.js AI patterns with Zapserp?
- Start with Server Components: Implement static content generation
- Add Edge Functions: Deploy globally distributed AI endpoints
- Implement Real-Time: Add collaborative features for user engagement
- Scale for Enterprise: Add multi-tenant support for B2B deployment
- Monitor Performance: Implement comprehensive monitoring and analytics
Need enterprise deployment guidance? Contact our team for architecture consultation and advanced integration support.