Next.js Integration
Complete examples for integrating WikiRest with Next.js 13+ using App Router and Server Components.
Setup
Environment variables
Add your API key to .env.local:
WIKIREST_API_KEY=wk_your_api_key API client
Create a reusable API client at lib/wikirest.ts:
const BASE_URL = "https://api.wikirest.com/v1";
interface SearchResult {
id: string;
page_id: number;
title: string;
section?: string;
text: string;
chunk_index: number;
url: string;
}
interface SearchResponse {
hits: SearchResult[];
query: string;
processingTimeMs: number;
estimatedTotalHits: number;
}
interface ChunkResponse {
id: string;
page_id: number;
title: string;
section?: string;
text: string;
chunk_index: number;
url: string;
word_count?: number;
modified?: string;
}
class WikiRestClient {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${BASE_URL}${path}`, {
...init,
headers: {
"X-API-Key": this.apiKey,
...init?.headers,
},
});
if (!response.ok) {
throw new Error(`WikiRest API error: ${response.status}`);
}
return response.json();
}
async search(query: string, limit = 10): Promise<SearchResponse> {
const params = new URLSearchParams({
q: query,
limit: String(limit),
});
return this.fetch(`/search?${params}`);
}
async getChunk(id: string): Promise<ChunkResponse> {
return this.fetch(`/chunk/${id}`);
}
async getPage(pageId: number) {
return this.fetch(`/page/${pageId}`);
}
}
export const wikirest = new WikiRestClient(
process.env.WIKIREST_API_KEY || ""
); Server Components
Search page with Server Component
Create a search page at app/search/page.tsx:
import { wikirest } from "@/lib/wikirest";
interface SearchPageProps {
searchParams: { q?: string };
}
export default async function SearchPage({ searchParams }: SearchPageProps) {
const query = searchParams.q || "";
let results = null;
let error = null;
if (query) {
try {
results = await wikirest.search(query, 10);
} catch (e) {
error = e instanceof Error ? e.message : "Search failed";
}
}
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Wikipedia Search</h1>
<form className="mb-8">
<input
type="search"
name="q"
defaultValue={query}
placeholder="Search Wikipedia..."
className="w-full px-4 py-2 border rounded-lg"
/>
</form>
{error && (
<div className="text-red-600 mb-4">{error}</div>
)}
{results && (
<div>
<p className="text-gray-600 mb-4">
Found {results.estimatedTotalHits} results in {results.processingTimeMs}ms
</p>
<div className="space-y-4">
{results.hits.map((hit) => (
<article key={hit.id} className="p-4 border rounded-lg">
<h2 className="text-xl font-semibold">
<a href={hit.url} className="text-blue-600 hover:underline">
{hit.title}
</a>
</h2>
{hit.section && (
<p className="text-sm text-gray-500">Section: {hit.section}</p>
)}
<p className="mt-2 text-gray-700">{hit.text}</p>
</article>
))}
</div>
</div>
)}
</div>
);
} Route Handlers
API route for client-side search
Create an API route at app/api/search/route.ts:
import { NextResponse } from "next/server";
import { wikirest } from "@/lib/wikirest";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get("q");
const limit = parseInt(searchParams.get("limit") || "10");
if (!query) {
return NextResponse.json(
{ error: "Query parameter 'q' is required" },
{ status: 400 }
);
}
try {
const results = await wikirest.search(query, limit);
return NextResponse.json(results);
} catch (error) {
console.error("Search error:", error);
return NextResponse.json(
{ error: "Search failed" },
{ status: 500 }
);
}
} Client Components
Interactive search with React Query
Use React Query for client-side search at components/SearchBox.tsx:
"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
async function searchWiki(query: string) {
if (!query) return null;
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
if (!response.ok) throw new Error("Search failed");
return response.json();
}
export function SearchBox() {
const [query, setQuery] = useState("");
const [searchTerm, setSearchTerm] = useState("");
const { data, isLoading, error } = useQuery({
queryKey: ["search", searchTerm],
queryFn: () => searchWiki(searchTerm),
enabled: !!searchTerm,
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSearchTerm(query);
};
return (
<div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search Wikipedia..."
className="flex-1 px-4 py-2 border rounded-lg"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
>
Search
</button>
</form>
{isLoading && <p className="mt-4">Searching...</p>}
{error && <p className="mt-4 text-red-600">Error: {error.message}</p>}
{data && (
<div className="mt-4 space-y-4">
{data.hits.map((hit: any) => (
<div key={hit.id} className="p-4 border rounded-lg">
<h3 className="font-semibold">{hit.title}</h3>
<p className="text-gray-600">{hit.text.slice(0, 200)}...</p>
</div>
))}
</div>
)}
</div>
);
} RAG with AI SDK
Using Vercel AI SDK for RAG
Build a RAG chatbot at app/api/chat/route.ts:
import { OpenAIStream, StreamingTextResponse } from "ai";
import OpenAI from "openai";
import { wikirest } from "@/lib/wikirest";
const openai = new OpenAI();
export async function POST(request: Request) {
const { messages } = await request.json();
const lastMessage = messages[messages.length - 1];
// Get relevant context from WikiRest
const searchResults = await wikirest.search(lastMessage.content, 5);
const context = searchResults.hits
.map((hit) => `## ${hit.title}\n${hit.text}\nSource: ${hit.url}`)
.join("\n\n---\n\n");
// Build system prompt with context
const systemPrompt = `You are a helpful assistant that answers questions using Wikipedia content.
Use the following Wikipedia excerpts to answer the user's question. Always cite your sources.
${context}
If the context doesn't contain relevant information, say so.`;
// Stream response from OpenAI
const response = await openai.chat.completions.create({
model: "gpt-4",
stream: true,
messages: [
{ role: "system", content: systemPrompt },
...messages,
],
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
} Chat component
Use the AI SDK React hooks at components/Chat.tsx:
"use client";
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div className="max-w-2xl mx-auto p-4">
<div className="space-y-4 mb-4">
{messages.map((message) => (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === "user"
? "bg-blue-100 ml-8"
: "bg-gray-100 mr-8"
}`}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask about anything on Wikipedia..."
className="flex-1 px-4 py-2 border rounded-lg"
/>
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isLoading ? "..." : "Send"}
</button>
</form>
</div>
);
} Caching strategies
Static generation with revalidation
Pre-render pages with ISR:
import { wikirest } from "@/lib/wikirest";
// Revalidate every hour
export const revalidate = 3600;
export default async function TopicPage({
params,
}: {
params: { topic: string };
}) {
const results = await wikirest.search(params.topic, 5);
return (
<div>
<h1>{params.topic}</h1>
{results.hits.map((hit) => (
<article key={hit.id}>
<h2>{hit.title}</h2>
<p>{hit.text}</p>
</article>
))}
</div>
);
} Using unstable_cache
Cache API responses with Next.js caching:
import { unstable_cache } from "next/cache";
import { wikirest } from "@/lib/wikirest";
const getCachedSearch = unstable_cache(
async (query: string, limit: number) => {
return wikirest.search(query, limit);
},
["wiki-search"],
{
revalidate: 3600, // 1 hour
tags: ["wiki"],
}
);
export default async function SearchPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const query = searchParams.q || "";
const results = query ? await getCachedSearch(query, 10) : null;
// ... render results
} Edge middleware
Rate limiting with Edge Middleware
Create middleware.ts to protect your API routes:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Simple in-memory rate limiting (use Redis in production)
const rateLimit = new Map<string, { count: number; resetTime: number }>();
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/search")) {
const ip = request.ip ?? "127.0.0.1";
const now = Date.now();
const windowMs = 60 * 1000; // 1 minute
const maxRequests = 20;
const record = rateLimit.get(ip);
if (record && record.resetTime > now) {
if (record.count >= maxRequests) {
return NextResponse.json(
{ error: "Rate limit exceeded" },
{ status: 429 }
);
}
record.count++;
} else {
rateLimit.set(ip, { count: 1, resetTime: now + windowMs });
}
}
return NextResponse.next();
}
export const config = {
matcher: "/api/:path*",
};