AI-Powered Search for Modern React Applications
Add AI-powered search to your Next.js app in minutes. Works with App Router, Server Components, and static export.
Building search in Next.js means choosing between client-only libraries that don't scale, or complex Elasticsearch setups that hurt performance.
Next.js has no built-in search solution. You're left building custom Elasticsearch setups or piecing together client-only libraries that don't scale.
Client-only search libraries cause hydration mismatches. Getting server-rendered search results with proper SEO requires complex workarounds.
Large datasets kill client-side search performance. Filtering thousands of records in the browser blocks the main thread and hurts Core Web Vitals.
No SDK to install — plain fetch calls to a REST API that work in Server Components, route handlers, and client components. Built for the modern Next.js stack.
Call the REST API with plain fetch from a Server Component. No SDK, no hydration issues, no use client directive needed for server-rendered results.
Fetch results in Server Components or route handlers for SEO-critical pages. Pre-render search results at build or request time.
No SDK types to learn. Responses are plain JSON (results, total, limit, offset), so a small interface describes your data shape.
Works with React 18 streaming and Suspense boundaries. Show search skeleton while results stream from the edge.
The search API allows direct browser calls (CORS), so a search-only key works with static export. No server required for search.
It is just fetch, so it runs on Vercel Edge Runtime and Cloudflare Workers. Sub-150ms search, called straight from the edge.
| Metric | Default Next.js | Algolia | SearchJet |
|---|---|---|---|
| Time to First Result | 1.5s+ | 200ms | <150ms |
| Typo Tolerance | None | Yes | AI-Powered |
| Monthly Cost (100K searches) | Free | $50/mo | $79/mo (Growth) |
| SSR / SEO Support | Manual | Partial | Full |
// app/search/page.jsx — a Server Component: results are in the HTML, no client JS
const API = 'https://app.searchjetengine.com/api/v1';
export default async function SearchPage({ searchParams }) {
const { q = '' } = await searchParams; // searchParams is a Promise in Next 15+
let data = { results: [], total: 0 };
if (q) {
const res = await fetch(
`${API}/search?q=${encodeURIComponent(q)}&limit=10`,
{
headers: { Authorization: `Bearer ${process.env.SEARCHJET_SEARCH_KEY}` },
cache: 'no-store',
}
);
if (res.ok) data = await res.json(); // { results: [...], total, limit, offset, ... }
}
return (
<main>
<form action="/search">
<input name="q" defaultValue={q} placeholder="Search docs..." />
</form>
{q && <p>{data.total} results for "{q}"</p>}
{data.results.map((hit) => (
<article key={hit.id} className="p-4 border-b">
<h3><a href={hit.url}>{hit.title}</a></h3>
<p>{hit.excerpt}</p>
</article>
))}
</main>
);
}// app/api/search/route.js — proxies search so the key stays in an env var
export async function GET(request) {
const q = request.nextUrl.searchParams.get('q') ?? '';
const res = await fetch(
`https://app.searchjetengine.com/api/v1/search?q=${encodeURIComponent(q)}&limit=10`,
{ headers: { Authorization: `Bearer ${process.env.SEARCHJET_SEARCH_KEY}` } }
);
return Response.json(await res.json(), { status: res.status });
}
// components/LiveSearch.jsx — search-as-you-type client component
'use client';
import { useEffect, useState } from 'react';
export default function LiveSearch() {
const [q, setQ] = useState('');
const [hits, setHits] = useState([]);
useEffect(() => {
if (!q) { setHits([]); return; }
const controller = new AbortController();
const timer = setTimeout(async () => {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
const data = await res.json();
setHits(data.results ?? []); // results is the array of hits
} catch {} // aborted or network error
}, 200); // debounce keystrokes
return () => { clearTimeout(timer); controller.abort(); };
}, [q]);
return (
<div>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search docs..." />
{hits.map((hit) => (
<article key={hit.id} className="p-4 border-b">
<h3><a href={hit.url}>{hit.title}</a></h3>
<p>{hit.excerpt}</p>
</article>
))}
</div>
);
}Need a complete starter project?
View GitHub ExamplesIllustrative example of a Next.js app adding hosted search. Your results will vary.
| Metric | Before | After | Impact |
|---|---|---|---|
| Setup Time | Weeks (self-built search) | Hours | Much faster to launch |
| Search Client Code | 80KB+ (Algolia) | None — built-in fetch | No SDK to ship |
| Search Speed | Slower on large datasets | <150ms | Faster results |
| Monthly Costs | Usage-based (Algolia) | $0-79 (Free to Growth) | Predictable flat pricing |
Everything you need to know about SearchJet for Next.js
Join developers building exceptional search experiences with SearchJet
Already have an account? Log in