# Building a Search Bar in Next.JS

Adding full-text search capabilities to your React/Next.js projects has never been easier. This walkthrough will take you through all the steps required to build a simple book search application using Next.js and the Typesense ecosystem.

# Prerequisites

This guide will use NextJS (opens new window), a React (opens new window) framework for building full-stack web applications.

Please ensure you have Node.js (opens new window) and Docker (opens new window) installed on your machine before proceeding. You will need it to run a typesense server locally and load it with some data. This will be used as a backend for this project.

This guide will use a Linux environment, but you can adapt the commands to your operating system.

# Step 1: Setup your Typesense server

Once Docker is installed, you can run a Typesense container in the background using the following commands:

  • Create a folder that will store all searchable data stored for Typesense:

    mkdir "$(pwd)"/typesense-data
    
  • Run the Docker container:

  • Verify if your Docker container was created properly:

    docker ps
    
  • You should see the Typesense container running without any issues:

    CONTAINER ID   IMAGE                      COMMAND                  CREATED       STATUS       PORTS                                         NAMES
    82dd6bdfaf66   typesense/typesense:latest   "/opt/typesense-serv…"   1 min ago   Up 1 minutes   0.0.0.0:8108->8108/tcp, [::]:8108->8108/tcp   nostalgic_babbage
    
  • That's it! You are now ready to create collections and load data into your Typesense server.

TIP

You can also set up a managed Typesense cluster on Typesense Cloud (opens new window) for a fully managed experience with a management UI, high availability, globally distributed search nodes and more.

# Step 2: Create a new books collection and load sample dataset into Typesense

Typesense needs you to create a collection in order to search through documents. A collection is a named container that defines a schema and stores indexed documents for search. Collection bundles three things together:

  1. Schema
  2. Document
  3. Index

You can create the books collection for this project using this curl command:

curl "http://localhost:8108/collections" \
      -X POST \
      -H "Content-Type: application/json" \
      -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
      -d '{
        "name": "books",
        "fields": [
          {"name": "title", "type": "string", "facet": false},
          {"name": "authors", "type": "string[]", "facet": true},
          {"name": "publication_year", "type": "int32", "facet": true},
          {"name": "average_rating", "type": "float", "facet": true},
          {"name": "image_url", "type": "string", "facet": false},
          {"name": "ratings_count", "type": "int32", "facet": true}
        ],
        "default_sorting_field": "ratings_count"
      }'

Now that the collection is set up, we can load the sample dataset.

  1. Download the sample dataset:

    curl -O https://dl.typesense.org/datasets/books.jsonl.gz
    
  2. Unzip the dataset:

    gunzip books.jsonl.gz
    
  3. Load the dataset in to Typesense:

    curl "http://localhost:8108/collections/books/documents/import" \
          -X POST \
          -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
          --data-binary @books.jsonl
    

You should see a bunch of success messages if the data load is successful.

Now you're ready to actually build the application.

# Step 3: Set up your Next.js project

Create a new Next.js project using this command:

npx create-next-app@latest typesense-next-search-bar

This will ask you a bunch of questions, just go with the default choices. It's good enough for most people.

Once your project scaffolding is ready, you need to install these three dependencies that will help you with implementing the search functionality. Use this command to install them:

npm i typesense typesense-instantsearch-adapter react-instantsearch

Let's go over these dependencies one by one:

  • typesense
    • Official JavaScript client for Typesense.
    • It isn't required for the UI, but it is needed if you want to interact with the Typesense server from Next.js API routes.
  • react-instantsearch
    • A react library from Algolia that provides ready-to-use UI components for building search interfaces.
    • Offers components like SearchBox, Hits and others that make displaying search results easy.
    • It also abstracts state management, URL synchronization and other complex stuff.
    • By itself, it's designed to work with Algolia's hosted search service and not Typesense.
  • typesense-instantsearch-adapter (opens new window)
    • This is the key library that acts as a bridge between the react-instantsearch and our self-hosted Typesense server.
    • This implements the InstantSearch.js adapter that react-instantsearch expects.
    • Translates the InstantSearch.js queries to Typesense API calls.

# Project Structure

Let's create the project structure step by step. After each step, we'll show you how the directory structure evolves.

  1. After creating the basic Next.js app and installing the required dependencies, your project structure should look like this:

    typesense-next-search-bar/
    ├── node_modules/
    ├── pages/
    │   └── index.tsx
    ├── public/
    │   ├── file.svg
    │   ├── globe.svg
    │   ├── next.svg
    │   ├── vercel.svg
    │   └── window.svg
    ├── .eslintrc.json
    ├── .gitignore
    ├── next-env.d.ts
    ├── next.config.ts
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    
  2. Create the lib directory and instantSearchAdapter.ts file:

    mkdir -p lib
    touch lib/instantSearchAdapter.ts
    

    Your project structure should now look like this:

    typesense-next-search-bar/
    ├── lib/
    │   └── instantSearchAdapter.ts
    ├── pages/
    │   └── index.tsx
    ├── public/
    │   ├── file.svg
    │   ├── globe.svg
    │   ├── next.svg
    │   ├── vercel.svg
    │   └── window.svg
    ├── .eslintrc.json
    ├── .gitignore
    ├── next-env.d.ts
    ├── next.config.ts
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    
  3. Copy this code into lib/instantSearchAdapter.ts:

    import TypesenseInstantsearchAdapter from 'typesense-instantsearch-adapter'
    
    export const typesenseInstantSearchAdapter = new TypesenseInstantsearchAdapter({
      server: {
        apiKey: process.env.NEXT_PUBLIC_TYPESENSE_API_KEY || '1234',
        nodes: [
          {
            host: process.env.NEXT_PUBLIC_TYPESENSE_HOST || 'localhost',
            port: parseInt(process.env.NEXT_PUBLIC_TYPESENSE_PORT || '8108'),
            protocol: process.env.NEXT_PUBLIC_TYPESENSE_PROTOCOL || 'http',
          },
        ],
      },
      additionalSearchParameters: {
        query_by: 'title,authors',
      },
    })
    

    This config file creates a reusable adapter that connects your React application to your Typesense Backend. It can take in a bunch of additional search parameters like sort by, number of typos, etc.

  4. Create the components directory and files:

    mkdir -p components
    touch components/SearchBar.tsx components/Searchbar.module.css
    touch components/BookList.tsx components/BookList.module.css
    touch components/BookCard.tsx components/BookCard.module.css
    

    Your project structure should now look like this:

    typesense-next-search-bar/
    ├── components/
    │   ├── BookCard.tsx
    │   ├── BookList.tsx
    │   ├── BookList.module.css
    │   ├── BookCard.module.css
    │   ├── SearchBar.tsx
    │   └── Searchbar.module.css
    ├── lib/
    │   └── instantSearchAdapter.ts
    ├── pages/
    │   └── index.tsx
    ├── public/
    │   ├── file.svg
    │   ├── globe.svg
    │   ├── next.svg
    │   ├── vercel.svg
    │   └── window.svg
    ├── .eslintrc.json
    ├── .gitignore
    ├── next-env.d.ts
    ├── next.config.ts
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    
  5. Let's create the SearchBar component. Add this to components/SearchBar.tsx:

    Note

    This walkthrough uses CSS Modules for styling. Since CSS is not the focus of this article, you can grab the complete stylesheets from the source code (opens new window).

    import { SearchBox } from 'react-instantsearch'
    import styles from './Searchbar.module.css'
    
    export const SearchBar = () => {
      return (
        <div className={styles.searchContainer}>
          <h1 className={styles.searchTitle}>Book Search</h1>
          <SearchBox
            placeholder='Search for books by title or author...'
            classNames={{
              form: styles.searchForm,
              input: styles.searchInput,
              submit: styles.searchButton,
              reset: styles.resetButton,
            }}
            submitIconComponent={() => (
              <svg
                className={styles.searchIcon}
                fill='none'
                stroke='currentColor'
                viewBox='0 0 24 24'
                xmlns='http://www.w3.org/2000/svg'
              >
                <path
                  strokeLinecap='round'
                  strokeLinejoin='round'
                  strokeWidth={2}
                  d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'
                />
              </svg>
            )}
            resetIconComponent={() => (
              <svg
                className={styles.closeIcon}
                fill='none'
                stroke='currentColor'
                viewBox='0 0 24 24'
                xmlns='http://www.w3.org/2000/svg'
              >
                <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
              </svg>
            )}
            loadingIconComponent={() => <div className={styles.loadingSpinner} />}
          />
        </div>
      )
    }
    

    The SearchBox component from react-instantsearch handles the search query internally through the InstantSearch context (opens new window). This component will be a child of the InstantSearch component and automatically passes the user's search query to the InstantSearch context. This approach automatically handles input management, debouncing, and state synchronization.

  6. Create the BookList component in components/BookList.tsx:

    import { useHits } from 'react-instantsearch'
    import type { Book } from '../types/Book'
    import { BookCard } from './BookCard'
    import styles from './BookList.module.css'
    
    export const BookList = () => {
      const { items } = useHits<Book>()
    
      if (!items || items.length === 0) {
        return (
          <div className={styles.emptyState}>
            {items ? 'No books found. Try a different search term.' : 'Start typing to search for books.'}
          </div>
        )
      }
    
      return (
        <div className={styles.bookList}>
          {items.map(item => (
            <BookCard key={item.objectID} book={item as unknown as Book} />
          ))}
        </div>
      )
    }
    

    This is a fairly simple component that will list all the search results obtained by the useHits hook. The useHits hook automatically connects to the nearest parent InstantSearch context and is subscribed to the state changes. It provides access to the current search results and additional metadata about the current search state.

  7. Create the BookCard component in components/BookCard.tsx:

    import type { Book } from '../types/Book'
    import styles from './BookCard.module.css'
    
    interface BookCardProps {
      book: Book
    }
    
    export const BookCard = ({ book }: BookCardProps) => {
      const { title, authors, publication_year, image_url, average_rating, ratings_count } = book
    
      return (
        <div className={styles.bookCard}>
          <div className={styles.bookImageContainer}>
            {image_url ? (
              <img
                src={image_url}
                alt={title}
                className={styles.bookImage}
                onError={e => {
                  ;(e.target as HTMLImageElement).src = '/book-placeholder.png'
                }}
              />
            ) : (
              <div className={styles.noImage}>No Image</div>
            )}
          </div>
          <div className={styles.bookInfo}>
            <h3 className={styles.bookTitle}>{title}</h3>
            <p className={styles.bookAuthor}>By: {authors?.join(', ')}</p>
            {publication_year && <p className={styles.bookYear}>Published: {publication_year}</p>}
            <div className={styles.ratingContainer}>
              <div className={styles.starRating}>{'★'.repeat(Math.round(average_rating || 0))}</div>
              <span className={styles.ratingText}>
                {average_rating?.toFixed(1)} ({ratings_count?.toLocaleString()} ratings)
              </span>
            </div>
          </div>
        </div>
      )
    }
    
  8. Create the types directory and Book type:

    mkdir -p types
    touch types/Book.ts
    

    Add this to types/Book.ts:

    export interface Book {
      objectID: string
      title: string
      authors: string[]
      publication_year: number
      average_rating: number
      image_url: string
      ratings_count: number
    }
    

    Your final project structure should now look like this:

    typesense-next-search-bar/
    ├── components/
    │   ├── BookCard.tsx
    │   ├── BookCard.module.css
    │   ├── BookList.tsx
    │   ├── BookList.module.css
    │   ├── SearchBar.tsx
    │   └── Searchbar.module.css
    ├── lib/
    │   └── instantSearchAdapter.ts
    ├── pages/
    │   └── index.tsx
    ├── public/
    │   ├── file.svg
    │   ├── globe.svg
    │   ├── next.svg
    │   ├── vercel.svg
    │   └── window.svg
    ├── types/
    │   └── Book.ts
    ├── .eslintrc.json
    ├── .gitignore
    ├── next-env.d.ts
    ├── next.config.ts
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json
    
  9. Finally, update your pages/index.tsx to use these components:

    import { InstantSearch } from 'react-instantsearch'
    import { typesenseInstantSearchAdapter } from '../lib/instantSearchAdapter'
    import { SearchBar } from '../components/SearchBar'
    import { BookList } from '../components/BookList'
    import Head from 'next/head'
    
    export default function Home() {
      return (
        <div className='min-h-screen bg-gray-50 py-8 px-4'>
          <Head>
            <title>Book Search with TypeSense</title>
            <meta name='description' content='Search through our collection of books' />
          </Head>
    
          <div className='max-w-7xl mx-auto'>
            <InstantSearch
              searchClient={typesenseInstantSearchAdapter.searchClient}
              indexName={process.env.NEXT_PUBLIC_TYPESENSE_INDEX || 'books'}
            >
              <SearchBar />
              <BookList />
            </InstantSearch>
          </div>
        </div>
      )
    }
    

    This is the main page that brings together all the required components. Notice that our SearchBar and BookList component are direct descendants of the InstantSearch component so that they have access to the InstantSearch context and vice-versa. Also notice that we pass the typesenseInstantsearchAdapter that we created in the lib directory as the searchClient to the InstantSearch component.

You've successfully built a search interface with Next.js and Typesense!

# Final Output

Here's how the final output should look like:

NextJS Search Bar Final Output

# Source Code

Here's the complete source code for this project on GitHub:

https://github.com/typesense/code-samples/tree/master/typesense-next-search-bar (opens new window)

Here's another related example that shows you how to build a search bar in a Next.JS application:

Guitar Chords Search with Next.js (opens new window)

# Need Help?

Read our Help section for information on how to get additional help.

Last Updated: 1/20/2026, 12:35:09 PM