# 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-dataRun the Docker container:
Verify if your Docker container was created properly:
docker psYou 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_babbageThat'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:
- Schema
- Document
- 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.
Download the sample dataset:
curl -O https://dl.typesense.org/datasets/books.jsonl.gzUnzip the dataset:
gunzip books.jsonl.gzLoad 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-instantsearchand our self-hosted Typesense server. - This implements the
InstantSearch.jsadapter thatreact-instantsearchexpects. - Translates the
InstantSearch.jsqueries to Typesense API calls.
- This is the key library that acts as a bridge between the
# Project Structure
Let's create the project structure step by step. After each step, we'll show you how the directory structure evolves.
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.jsonCreate the
libdirectory andinstantSearchAdapter.tsfile:mkdir -p lib touch lib/instantSearchAdapter.tsYour 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.jsonCopy 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.
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.cssYour 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.jsonLet's create the
SearchBarcomponent. Add this tocomponents/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
SearchBoxcomponent fromreact-instantsearchhandles the search query internally through the InstantSearch context (opens new window). This component will be a child of theInstantSearchcomponent and automatically passes the user's search query to theInstantSearchcontext. This approach automatically handles input management, debouncing, and state synchronization.Create the
BookListcomponent incomponents/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
useHitshook. TheuseHitshook automatically connects to the nearest parentInstantSearchcontext and is subscribed to the state changes. It provides access to the current search results and additional metadata about the current search state.Create the
BookCardcomponent incomponents/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> ) }Create the types directory and Book type:
mkdir -p types touch types/Book.tsAdd 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.jsonFinally, update your
pages/index.tsxto 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
SearchBarandBookListcomponent are direct descendants of theInstantSearchcomponent so that they have access to theInstantSearchcontext and vice-versa. Also notice that we pass thetypesenseInstantsearchAdapterthat we created in the lib directory as thesearchClientto theInstantSearchcomponent.
You've successfully built a search interface with Next.js and Typesense!
# Final Output
Here's how the final output should look like:

# 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)
# Related Examples
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.