Skip to content
SearchJet
Headless Search API for Next.js

Search API Calls from App Router & Server Components

Server-side search without hydration issues. Route-handler proxies, SSG caching, and edge-compatible fetch — the headless option for teams who own their UI.

The Headless API — Live

This is what your Next.js route handler will return. Type a query below.

app.js
const params = new URLSearchParams({ q: 'wireless headphones', limit: '5' });

// Search as you type → instant results, no SDK needed
const res = await fetch(`https://app.searchjetengine.com/api/v1/search?${params}`, {
  headers: { Authorization: 'Bearer 8e371cadae7aefedd3c99e41835df7ad709a27128c2a2d0c10ac408b1124c5a7' }
});
console.log(await res.json());   // ← live JSON below
REST API · no SDK neededStep 1 · call the API
Try:
Step 2 · response.json()
{
  // Run a query above to see the live
  // JSON response from the SearchJet API.
  // Typo-tolerant, sub-150ms.
}
Type a query → watch the JSON update
Live product-search index powered by a real public key. Queries run through a cached proxy.
Run it on your own index
Why Go Headless?

Full Control, Zero Search Infrastructure

Keep the API key secret

Search keys live in a route handler (server-only), never in the client bundle. Your index and quota stay protected.

Server components stay fast

Fetch search results inside a React Server Component or ISR-revalidate them at build time. No client-side waterfall, no hydration mismatch.

Cache at the edge

Tell Next.js to cache responses with revalidate / cache tags. Cached responses skip the round trip entirely, and uncached SearchJet queries return in under 150ms.

Copy-Paste Integration

Three Files. That's the Whole Setup.

1 · Route handler (key stays server-side)

app/api/search/route.ts
// app/api/search/route.ts — server-only, key stays private
import { NextResponse } from 'next/server';

const SEARCHJET_API = 'https://app.searchjetengine.com/api/v1/search';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const q = searchParams.get('q')?.trim();
  if (!q) return NextResponse.json({ error: 'Missing q' }, { status: 400 });

  const res = await fetch(`${SEARCHJET_API}?q=${encodeURIComponent(q)}&limit=8`, {
    headers: {
      // Search key from .env (no NEXT_PUBLIC_ prefix). Never use the private key here.
      Authorization: `Bearer ${process.env.SEARCHJET_API_KEY}`,
    },
    // Cache repeat queries — 60s revalidation
    next: { revalidate: 60 },
  });

  const data = await res.json();
  if (!res.ok) return NextResponse.json(data, { status: res.status });

  // `results` is the array of hits; `total` is the number of matches
  return NextResponse.json({ results: data.results, total: data.total });
}

2 · Client search component

components/SiteSearch.tsx
'use client';

import { useState, useEffect } from 'react';

type Hit = { id: string; title: string; url: string; excerpt?: string };

export default function SiteSearch() {
  const [query, setQuery] = useState('');
  const [hits, setHits] = useState<Hit[]>([]);

  useEffect(() => {
    const q = query.trim();
    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 ?? []);
      } catch (err) {
        if ((err as Error).name !== 'AbortError') console.error(err);
      }
    }, 150); // debounce

    return () => { clearTimeout(timer); controller.abort(); };
  }, [query]);

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search…"
      />
      <ul>
        {hits.map((hit) => (
          <li key={hit.id}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        ))}
      </ul>
    </>
  );
}

3 · React Server Component (optional SSR)

app/search/page.tsx
// app/search/page.tsx — React Server Component (Next.js 15+)
type Hit = { id: string; title: string; url: string; excerpt?: string };

export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q = '' } = await searchParams; // a Promise in Next.js 15+
  let results: Hit[] = [];
  let total = 0;

  if (q) {
    // Runs on the server — key never reaches the browser
    const res = await fetch(
      `https://app.searchjetengine.com/api/v1/search?q=${encodeURIComponent(q)}&limit=10`,
      {
        headers: { Authorization: `Bearer ${process.env.SEARCHJET_API_KEY}` },
        next: { revalidate: 60 },
      }
    );
    if (res.ok) {
      const data = await res.json();
      results = data.results; // array of hits
      total = data.total;
    }
  }

  return (
    <main>
      <form action="/search">
        <input name="q" defaultValue={q} placeholder="Search…" />
      </form>
      {q && <p>{total} results for “{q}”</p>}
      <ul>
        {results.map((hit) => (
          <li key={hit.id}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        ))}
      </ul>
    </main>
  );
}

Frequently Asked Questions

What does "headless" search mean for Next.js?
Headless means SearchJet provides a plain REST search endpoint — no SDK, no UI imposed. One HTTP request with any HTTP client (fetch works everywhere). You call the API from route handlers or server components and render results with your own components, exactly how you want them.
How do I avoid exposing my API key in the browser?
Put the key in an environment variable and call SearchJet from an App Router route handler (/app/api/search/route.ts). The client only ever sees your own endpoint. The example below shows the complete pattern.
Does headless search work with React Server Components?
Yes. Call GET /search with fetch inside a server component, using the key from an environment variable. Combine with Next.js stale-while-revalidate caching so repeated queries hit the cache instead of the origin.
Can I cache search results with ISR / SSG?
Yes — the search route example includes a revalidate option. A 5-minute revalidation window gives you near-instant repeat searches while keeping results fresh.
Is there a simpler, UI-bundled option?
There is no SDK or component library to install — every integration is the same single HTTP request. For a ready-made pattern, copy the React autocomplete or vanilla JS examples in our other guides and style them to match your site.

Headless Search, Delivered

10,000 free searches/month. Built for the App Router.

More Search Recipes