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
npm install meilisearch
Copy the Endpoint URL and API key from your instance's Connection details at app.celersearch.com:
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.
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
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.
npm install @meilisearch/instant-meilisearch react-instantsearch
// 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 })
}
'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
- Connect Any App: SDK basics, platform responses and limits
- Migrate to CelerSearch: move an existing index from another Meilisearch
- Pricing: plans by document count, with unlimited searches