Next.js

There are two ways to search a CelerSearch instance from a Next.js App Router app:

  • Server Components query the instance directly with the official SDK. The key never leaves the server.
  • React InstantSearch gives you a search-as-you-type UI. Its requests go through a route handler in your app, which adds the key on the server.

1. Install and configure

Shell
npm install meilisearch

Copy the Endpoint URL and API key from your instance's Connection details at app.celersearch.com:

.env.local
Shell
CELERSEARCH_URL=https://your-endpoint-url
CELERSEARCH_API_KEY=your-api-key

No NEXT_PUBLIC_ prefix

The API key has full access to your instance. Without the NEXT_PUBLIC_ prefix, Next.js keeps these variables on the server. Never add the prefix to the key.

lib/search.ts
TypeScript
import { Meilisearch } from 'meilisearch'

export const search = new Meilisearch({
  host: process.env.CELERSEARCH_URL!,
  apiKey: process.env.CELERSEARCH_API_KEY!,
})

Index your content with the same client from a script, a route handler or a background job. See Connect Any App for addDocuments.

2. Search in a Server Component

app/search/page.tsx
TSX
import { search } from '@/lib/search'

export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>
}) {
  const { q = '' } = await searchParams
  const { hits } = await search.index('movies').search(q, { limit: 20 })

  return (
    <main>
      <form>
        <input name="q" defaultValue={q} placeholder="Search movies" />
      </form>
      <ul>
        {hits.map((hit) => (
          <li key={hit.id}>{hit.title}</li>
        ))}
      </ul>
    </main>
  )
}

Submitting the form reloads the page with ?q=, and the search runs on the server.

3. Instant search with React InstantSearch

instant-meilisearch connects InstantSearch to Meilisearch. It sends searches as POST /multi-search and searchable facet lists as POST /indexes/{uid}/facet-search. Point it at a route handler in your app instead of at CelerSearch, and the handler forwards the request with the real key.

Shell
npm install @meilisearch/instant-meilisearch react-instantsearch
app/api/search/multi-search/route.ts
TypeScript
// Only these indexes can be searched from the browser.
const ALLOWED_INDEXES = new Set(['movies'])

export async function POST(request: Request) {
  const { queries } = await request.json()

  if (
    !Array.isArray(queries) ||
    queries.length === 0 ||
    !queries.every((query) => ALLOWED_INDEXES.has(query?.indexUid))
  ) {
    return Response.json({ message: 'Index not allowed' }, { status: 403 })
  }

  const response = await fetch(`${process.env.CELERSEARCH_URL}/multi-search`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.CELERSEARCH_API_KEY}`,
    },
    body: JSON.stringify({ queries }),
  })

  return Response.json(await response.json(), { status: response.status })
}
app/search/instant/search-ui.tsx
TSX
'use client'

import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'
import { Hits, InstantSearch, SearchBox } from 'react-instantsearch'

// The SDK needs an absolute URL: your own app, not CelerSearch.
const { searchClient } = instantMeiliSearch(
  `${process.env.NEXT_PUBLIC_SITE_URL}/api/search`,
  '', // No key in the browser; the route handler adds it.
)

export function SearchUI() {
  return (
    <InstantSearch indexName="movies" searchClient={searchClient}>
      <SearchBox placeholder="Search movies" />
      <Hits hitComponent={({ hit }) => <p>{String(hit.title)}</p>} />
    </InstantSearch>
  )
}

Set NEXT_PUBLIC_SITE_URL to your app's own origin, for example http://localhost:3000 in development. Render <SearchUI /> from any page.

The route handler does the job a search-only key does elsewhere: the browser can search the indexes you allow and nothing else. Anything in those indexes' displayed attributes is visible to visitors, so keep private fields out of them. Using a searchable RefinementList? Add a second handler that forwards POST /indexes/{uid}/facet-search with the same index check.

Next steps