WikiRest Docs

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*",
};

Next steps

Was this page helpful?

Help us improve our documentation