Build a Next.js App

Get Started

In this walkthrough we are going to get started building a search experience with Next.js.

You don't need to use Next.js to use Searchkit, but it is the easiest way to get started.

In this walkthrough, we will:

  • Setup an api route to fetch results from Elasticsearch
  • Use React InstantSearch to display the results 🎉

Download an Example Project

You can check out a Next.js project with Searchkit here:

curl | \
tar -xz --strip=2 searchkit-main/examples/with-ui-nextjs-react

or view the example codebase on github here (opens in a new tab)

Code Sandbox Example

You can also check out the code sandbox example here:

Create a Next.js app


This tutorial will use the new Next.js App Router. If you're using pages, keep this in mind when following along.

First, we need to create a Next.js app. We can do this by running the following command:

npx create-next-app@latest

and follow the instructions.

Navigate to the newly created directory.

Install Dependencies

Next we need to install the dependencies for this project:

  npm install @searchkit/instantsearch-client @searchkit/api react-instantsearch

Setup the Node API

Create a new file in the app/api/search directory called route.ts and add the following code:

import Client from "@searchkit/api";
import { NextRequest, NextResponse } from 'next/server'
const apiConfig = {
  connection: {
    host: "<replace-with-your-elasticsearch-host>",
    // if you are authenticating with an api key
    // apiKey: '###'
    // if you are authenticating with a username/password combo
    // auth: {
    //   username: "elastic",
    //   password: "changeme"
    // },
  search_settings: {
    highlight_attributes: ["title", "actors"],
    search_attributes: ["title", "actors"],
    result_attributes: ["title", "actors"],
    facet_attributes: ["type", "rated"],
const apiClient = Client(apiConfig);
export async function POST(req: NextRequest, res: NextResponse) {
  const data = await req.json()
  const results = await apiClient.handleRequest(data)
  return NextResponse.json(results)

Replace the host and apiKey with your Elasticsearch host and API key. The apiKey is optional, but recommended for production environments. You can find more information about the API key here (opens in a new tab).

This will setup a new Next.js route handler (opens in a new tab) under the /api/search path. This route will handle the search requests and use the InstantSearch Elasticsearch Adapter to handle the requests. The response is then returned back to the client.

For more information on API configuration, see the API Configuration docs.

Setup the Frontend

Now that we have the API setup, we can start building the frontend. We will use react-instantsearch (opens in a new tab) to build the search experience.

First, we need to create a new file (if it doesn't already exist) in the app directory called page.tsx and add the following code:

import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
import createClient from "@searchkit/instantsearch-client";
const searchClient = createClient({
  url: "/api/search",
export default function Search() {
  return (
      indexName="<elasticsearch index or alias name>"
      <SearchBox />
      <Hits />

Instantsearch will use the searchClient to make requests to the API we created earlier. The indexName is the name of the index we want to search.

Run the app

Now that we have everything setup, we can run the app and see the search experience in action.

npm run dev


Searchable Attributes

Now that we have the search experience setup, we can add additional search functionality.

Adjusting the search fields

we can adjust the search fields by updating the search_attributes in the apiConfig object in the app/api/search/route.ts file.

  search_attributes: ["title^3", "actors", "plot"],

Above we have boosted title by 3 times. This means that the title will have a higher weight than the other fields. This will make sure that the title has a higher importance in the search results.

Overriding the Default Query

We can optionally override the default search query by implementing the getQuery function in the handleRequest method called in the app/api/search/route.ts file.

This function will receive the query and the function will return the Elasticsearch query that will be used to search the index.

const results = await apiClient.handleRequest(body, {
  getQuery: (query, search_attributes) => {
    return [
        combined_fields: {
          fields: search_attributes,

Customizing the Results Hit

We can add a custom hit component to display the results. We can create a new file called Hit.ts in the components directory and add the following code:

Below we are using the Highlight component from react-instantsearch to highlight the search term in the title and actors fields.

import { Highlight } from "react-instantsearch";
const hitView = (props) => {
  return (
        <Highlight hit={props.hit} attribute="title" />
      <br />
      <Highlight hit={props.hit} attribute="actors" />

We need to pass the attribute prop to the highlight_attributes config to tell which fields to bring highlight options for.

  highlight_attributes: ["title", "actors"],

Then we can import the Hit component in the app/page.tsx file and pass it to the parent Hits component.

import Hit from "../components/Hit";
export default function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="movies">
      <SearchBox />
      <Hits hitComponent={Hit} />


Adding a Refinement List Facet

Start by updating the apiConfig object in the app/api/search/route.ts file to add the type facet.

  facet_attributes: [{ attribute: "type", "type": "string" }],

This assumes there is a type field in the index that is a keyword type field.

If the field is a text type field, you can define and use the type.keyword subfield instead.

  facet_attributes: [{ attribute: "type", field: "type.keyword", type: "string" }],

Then we can add the RefinementList component to the pages/search.js file.

import {
} from "react-instantsearch";
export default function Search() {
  return (
    <InstantSearch searchClient={searchClient} indexName="movies">
      <SearchBox />
      <RefinementList attribute="type" />
      <Hits hitComponent={Hit} />

Make it searchable

By default, the RefinementList component will show all the values for the facet. We can make it searchable by adding the searchable prop.

<RefinementList attribute="type" searchable />

Adding a Numeric Range based Facet

Start by updating the apiConfig object in the app/page.tsx file to add the imdbrating facet. This requires the imdbrating field to be a numeric type field like a float in the Elasticsearch index.

facet_attributes: [
  { attribute: "imdbrating", type: "numeric" },
  { attribute: "type", field: "type.keyword", type: "string" }

Then we can add the RangeInput component to the app/page.tsx file.

import {
} from "react-instantsearch";

Server Side Rendering

Below we add the following additional imports:

  1. The getServerState function from react-instantsearch
  2. The renderToString function from react-dom/server
  3. The InstantSearchServerState and InstantSearchSSRProvider components from react-instantsearch
  4. The createInstantSearchRouterNext function from react-instantsearch-router-nextjs
  5. The singletonRouter from next/router

Then we wrap the InstantSearch component with the InstantSearchSSRProvider component and pass the serverState prop to it.

This allows us to render the search experience on the server and send the initial state to the client. This will make the search experience load faster and also improve SEO.

import { 
  InstantSearch, SearchBox, Hits, RefinementList, RangeInput, 
  InstantSearchServerState, InstantSearchSSRProvider, getServerState
} from 'react-instantsearch';
import { renderToString } from 'react-dom/server';
import Client from '@searchkit/instantsearch-client'
import { GetServerSideProps } from 'next';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
import singletonRouter from 'next/router';
type WebProps = {
  serverState?: InstantSearchServerState;
  url?: string;
  serverUrl?: string;
export default function Web({ serverState, url, serverUrl }: WebProps) {
    const searchClient = Client({
      url: serverUrl + '/api/product-search',
    return (
      <InstantSearchSSRProvider {...serverState}>
        <div className="ais-InstantSearch">
          <InstantSearch searchClient={searchClient} indexName="movies">
            <SearchBox />
            <RefinementList attribute="type" searchable />
            <RangeInput attribute="imdbrating" />
            <Hits hitComponent={Hit} />
export const getServerSideProps: GetServerSideProps<WebProps> =
  async function getServerSideProps({ req }) {
    const protocol = req.headers.referer?.split('://')[0] || 'http';
    const serverUrl = `${protocol}://${}`;
    const url = `${protocol}://${}${req.url}`;
    const serverState = await getServerState(<Web url={url} serverUrl={serverUrl} />, {
    return {
      props: {


We have quickly built a really nice search experience from scratch using Elasticsearch and Algolia InstantSearch. We have also learned how to customize the search experience by adjusting the search fields and overriding the default Elasticsearch query.

Apache 2.0 2024 © Joseph McElroy.
Need help? Join discord