Algolia Search in Nuxt 3: Production-Ready Integration Guide
If you’ve ever tried to build a search feature from scratch — tokenizing queries, writing ranking logic, handling typos, managing indices — you’ve probably ended up somewhere between “this is fine” and a full existential crisis.
Algolia search exists precisely to prevent that moment. It’s a hosted search-as-a-service platform that gives you millisecond-speed, typo-tolerant, relevance-ranked full-text search through a clean REST API and a rich JavaScript SDK — all without managing a single Elasticsearch cluster at 3 AM.
Nuxt 3, on the other hand, is the modern full-stack framework built on top of Vue 3 and Vite. It ships with a powerful composables system, native SSR/SSG capabilities, and a module ecosystem that makes integrating third-party services feel less like plumbing and more like configuration.
Combining Nuxt 3 with Algolia gives you a production-grade web app search system that scales from a personal blog to an enterprise e-commerce platform.
This guide walks you through the complete Algolia Nuxt integration: from installing the @nuxtjs/algolia module and configuring your index, to implementing composables like useAlgoliaSearch and useAsyncAlgoliaSearch, rendering results with Vue InstantSearch, and handling the always-fun SSR edge cases. By the end, you’ll have a fully functional, SEO-friendly Nuxt 3 search implementation ready to ship.
Why Algolia Is the Right Choice for Nuxt 3 Applications
Before touching a single line of code, it’s worth understanding what makes Algolia instant search different from rolling your own solution or using a database LIKE query. Algolia operates on pre-built, optimized indices stored on its distributed infrastructure. When a user types a query, the request hits an Algolia edge node — not your origin server — and returns results typically within 10–50 milliseconds. For a Vue search engine integrated into a Nuxt app, this means your UI stays snappy even under high load without you having to spin up additional compute.
The Algolia API search layer is genuinely well-designed. You get a RESTful interface and a JavaScript client that handles retries, caching, and request deduplication out of the box. The algoliasearch npm package is the official client, and vue-instantsearch wraps it into a set of reactive Vue 3 components that dramatically reduce the amount of UI code you’d otherwise write. You’re not just getting a fast search backend — you’re getting a full toolkit for building faceted search, autocomplete, geo-search, and even Algolia Recommendations API-powered “you might also like” blocks.
From a TypeScript search integration standpoint, Algolia’s JavaScript client ships with first-class TypeScript types. Combined with Nuxt 3’s native TypeScript support, you get full type safety across your search implementation — from index hit types to composable return values. This matters more than people initially expect: a typed SearchResponse<YourHitType> catches a surprising number of runtime bugs at compile time.
Setting Up the @nuxtjs/algolia Module
The official @nuxtjs/algolia module is the recommended entry point for any Nuxt search module integration. It wraps the core Algolia JavaScript client, registers composables globally, and handles the module lifecycle properly within Nuxt’s plugin system — so you don’t need to manually initialize the client or worry about SSR context.
Start by installing the module and the peer dependency:
# Using npm
npm install @nuxtjs/algolia
# Using pnpm (recommended for Nuxt 3)
pnpm add @nuxtjs/algolia
Next, register it in your nuxt.config.ts and provide your credentials. Never hardcode your Application ID or API key directly — use Nuxt’s runtimeConfig to inject them from environment variables:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/algolia'],
algolia: {
applicationId: process.env.ALGOLIA_APPLICATION_ID,
apiKey: process.env.ALGOLIA_SEARCH_API_KEY,
// Optional: enable lite client for smaller bundle
lite: true,
// Optional: cache configuration
cache: false,
// Optional: instantSearch integration
instantSearch: {
theme: 'algolia'
}
}
})
Create a .env file in your project root with your Algolia credentials. Use the Search-Only API Key — never the Admin API Key — in your frontend configuration:
ALGOLIA_APPLICATION_ID=YourAppID
ALGOLIA_SEARCH_API_KEY=YourSearchOnlyAPIKey
Understanding useAlgoliaSearch and useAsyncAlgoliaSearch Composables
The @nuxtjs/algolia module exposes two primary Nuxt 3 composables for querying your Algolia index search: useAlgoliaSearch and useAsyncAlgoliaSearch. They look nearly identical on the surface but serve fundamentally different roles in the Nuxt data-fetching lifecycle, and confusing them is the single most common source of SSR bugs in this integration.
useAlgoliaSearch is a client-side composable. It gives you a reactive search function and a result ref that updates whenever you call it. Use it for interactive, user-driven search experiences — think a search bar with a @input handler or a debounced live search. It returns an object with result, search, and status, making it straightforward to bind to a Nuxt search UI:
<!-- components/SearchBar.vue -->
<script setup lang="ts">
import type { SearchResponse } from '@algolia/client-search'
interface ProductHit {
objectID: string
name: string
description: string
price: number
category: string
}
const { result, search, status } = useAlgoliaSearch('products_index')
const query = ref('')
const handleSearch = useDebounceFn(async () => {
await search({ query: query.value })
}, 300)
watch(query, handleSearch)
</script>
<template>
<div class="search-container">
<input
v-model="query"
type="search"
placeholder="Search products..."
aria-label="Search"
/>
<div v-if="status === 'loading'">Searching...</div>
<ul v-if="result?.hits?.length">
<li v-for="hit in result.hits" :key="hit.objectID">
<strong>{{ hit.name }}</strong>
<span>{{ hit.price }}</span>
</li>
</ul>
<p v-else-if="query && status === 'success'">
No results found for "{{ query }}"
</p>
</div>
</template>
useAsyncAlgoliaSearch is designed for server-side search scenarios. It wraps the query inside Nuxt’s useAsyncData, which means it runs on the server during SSR, serializes the result into the page payload, and hydrates on the client without a second network request. This is the correct choice for pre-rendered search results pages, category pages with default queries, or any page where search results need to be in the initial HTML for SEO purposes:
<!-- pages/blog/index.vue -->
<script setup lang="ts">
const { data: searchResult } = await useAsyncAlgoliaSearch({
indexName: 'blog_posts',
query: '',
requestOptions: {
hitsPerPage: 12,
attributesToRetrieve: ['title', 'slug', 'excerpt', 'publishedAt'],
filters: 'status:published'
}
})
</script>
<template>
<section>
<article
v-for="post in searchResult?.hits"
:key="post.objectID"
>
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
<NuxtLink :to="`/blog/${post.slug}`">Read more</NuxtLink>
</article>
</section>
</template>
The mental model is simple: if the search happens in response to a user action, use useAlgoliaSearch. If the search populates the initial page render and you want it in the HTML, use useAsyncAlgoliaSearch. Mixing them up doesn’t break the app — but it either produces a content flash on page load or makes search results invisible to crawlers, neither of which is ideal for a production Nuxt 3 setup.
Implementing Vue InstantSearch for Rich Search UI
If you need a full-featured search interface — real-time results as you type, filtering panels, pagination, sorting controls — writing all of that from scratch with raw composables would be tedious. This is where Vue InstantSearch enters the picture. It’s Algolia’s official component library for Vue 3, providing a set of pre-built, fully accessible, and highly customizable widgets that connect directly to the Algolia search state machine.
The key gotcha with Vue InstantSearch in Nuxt 3 is hydration. The InstantSearch components manage their own internal state and rely on browser APIs, which makes them incompatible with Nuxt’s default SSR rendering without a wrapper. The solution is simple — wrap your InstantSearch implementation in a <ClientOnly> component, or better yet, load it as a client-only plugin. Here’s the full setup:
// plugins/vue-instantsearch.client.ts
import InstantSearch from 'vue-instantsearch/vue3/es'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(InstantSearch)
})
<!-- pages/search.vue -->
<script setup lang="ts">
import algoliasearch from 'algoliasearch/lite'
import { AisInstantSearch, AisSearchBox, AisHits, AisRefinementList, AisPagination } from 'vue-instantsearch/vue3/es'
const runtimeConfig = useRuntimeConfig()
const searchClient = algoliasearch(
runtimeConfig.public.algoliaApplicationId,
runtimeConfig.public.algoliaApiKey
)
</script>
<template>
<ClientOnly>
<AisInstantSearch
:search-client="searchClient"
index-name="products_index"
>
<div class="search-layout">
<aside class="search-filters">
<h3>Category</h3>
<AisRefinementList attribute="category" />
<h3>Brand</h3>
<AisRefinementList attribute="brand" :limit="10" />
</aside>
<main class="search-results">
<AisSearchBox placeholder="Search products..." />
<AisHits>
<template #item="{ item }">
<article class="hit-card">
<img :src="item.image" :alt="item.name" />
<h2>{{ item.name }}</h2>
<p>\${{ item.price }}</p>
</article>
</template>
</AisHits>
<AisPagination />
</main>
</div>
</AisInstantSearch>
<template #fallback>
<div class="search-skeleton">Loading search...</div>
</template>
</ClientOnly>
</template>
This pattern gives you the full algolia faceted search experience — hierarchical categories, multi-select filters, dynamic sorting — with zero custom state management. The InstantSearch router integration can also sync filter state to URL query parameters, making filtered search results shareable and bookmarkable. That last feature alone saves you a couple of days of custom routing code.
Nuxt Server Side Search and SSR Considerations
The Algolia SSR search story in Nuxt 3 is more nuanced than most tutorials suggest. The pure SSR approach — fetching search results server-side and embedding them in HTML — works perfectly for static or semi-static queries: a “latest articles” block, a category landing page, a featured products section. These are excellent candidates for useAsyncAlgoliaSearch because the query doesn’t change based on user input, the results benefit from being indexed by search engines, and the page loads with content already visible.
For dynamic, interactive search — where users type queries and see results update in real time — SSR adds latency without meaningful benefit. A live search bar doesn’t need to be server-rendered; it doesn’t exist in a meaningful state before the user types something. The right pattern here is to use the SSR composable for the initial page state (empty query, default results, or featured content) and switch to the client-side useAlgoliaSearch or Vue InstantSearch once the component mounts. This hybrid approach is what production Nuxt 3 setups actually use:
<!-- pages/products/[category].vue -->
<script setup lang="ts">
const route = useRoute()
// SSR: pre-render default category results for SEO
const { data: initialResults } = await useAsyncAlgoliaSearch({
indexName: 'products_index',
query: '',
requestOptions: {
filters: `category:${route.params.category}`,
hitsPerPage: 24
}
})
// Client: reactive search state for user interactions
const { result: searchResult, search } = useAlgoliaSearch('products_index')
const query = ref('')
const isUserSearching = computed(() => query.value.length > 0)
// Display SSR results until user starts searching
const displayedResults = computed(() =>
isUserSearching.value ? searchResult.value : initialResults.value
)
</script>
There’s one more server-side consideration worth mentioning: rate limits and Algolia plan quotas. The Algolia search API counts every search request against your monthly operation quota. In a high-traffic SSR application, every page render that calls useAsyncAlgoliaSearch consumes an operation. If you’re on a free plan (10,000 operations/month), a popular category page could exhaust your quota in hours. The solution is either caching the Nuxt server route response with routeRules, using Nuxt’s server-side caching with cachedFunction, or upgrading to a paid Algolia plan. Plan ahead — or be surprised.
Indexing Your Data: Getting Content Into Algolia
A search engine is only as good as its index, and before any of the frontend code matters, you need to get your data into Algolia. Indexing happens through the Admin API Key — which, to reiterate, never goes into your frontend bundle. The typical pattern in a Nuxt 3 project is to run indexing from a server-side script, a Nuxt server route, or a CI/CD pipeline step.
Here’s a minimal Node.js indexing script using the official algoliasearch client. The pattern works identically in a Nuxt server route at server/api/reindex.post.ts:
// scripts/index-products.ts
import algoliasearch from 'algoliasearch'
interface ProductRecord {
objectID: string
name: string
description: string
price: number
category: string
brand: string
slug: string
image: string
}
const client = algoliasearch(
process.env.ALGOLIA_APPLICATION_ID!,
process.env.ALGOLIA_ADMIN_API_KEY! // Admin key — server only!
)
const index = client.initIndex('products_index')
async function reindexProducts() {
// Fetch from your CMS, database, or API
const products: ProductRecord[] = await fetchAllProducts()
// Configure index settings
await index.setSettings({
searchableAttributes: [
'name',
'description',
'category',
'brand'
],
attributesForFaceting: [
'filterOnly(category)',
'filterOnly(brand)',
'price'
],
customRanking: ['desc(popularity)', 'asc(price)'],
attributesToHighlight: ['name', 'description']
})
// Batch save — Algolia recommends batches of 1000 records
const { objectIDs } = await index.saveObjects(products)
console.log(`Indexed ${objectIDs.length} products`)
}
reindexProducts().catch(console.error)
The objectID field is Algolia’s primary key — it should be a stable, unique identifier from your data source (a database ID, a CMS entry ID, or a slug). Algolia will automatically replace records with the same objectID during re-indexing, which makes incremental updates straightforward. For large datasets, the index.saveObjects method batches records automatically at 1000 objects per API call.
Algolia Recommendations API in Nuxt 3
Beyond search, Algolia offers the Algolia Recommendations API — a machine learning-powered service that generates “Related Products”, “Frequently Bought Together”, and “Trending Items” recommendations based on user event data. Integrating it into a Nuxt 3 application follows the same module pattern, but requires a separate client package and event instrumentation setup.
To use the Recommendations API, you first need to send user events (views, clicks, conversions) to Algolia’s Insights API. The @nuxtjs/algolia module includes optional Insights support that can be enabled globally. Once you’ve accumulated enough event data (typically a few thousand events), you can request recommendations through the @algolia/recommend client:
// server/api/recommendations/[productId].get.ts
import recommend from '@algolia/recommend'
export default defineEventHandler(async (event) => {
const productId = getRouterParam(event, 'productId')
const recommendClient = recommend(
process.env.ALGOLIA_APPLICATION_ID!,
process.env.ALGOLIA_SEARCH_API_KEY!
)
const { results } = await recommendClient.getRelatedProducts([{
indexName: 'products_index',
objectID: productId!,
maxRecommendations: 6
}])
return results[0]?.hits ?? []
})
This server route safely uses your Search API Key (the Recommendations API doesn’t require Admin access for reads), returns a clean array of product hits, and can be called from any component with useFetch('/api/recommendations/product-123'). The server-side execution keeps your implementation clean and adds a layer of caching opportunity through Nuxt’s server-side cache utilities.
Production Setup: Environment, Performance, and Monitoring
Shipping a working prototype to production without hardening it is a rite of passage in web development, and Algolia integrations have their own set of production-specific concerns. The first is bundle size. The full algoliasearch client is around 30kB gzipped, but the lite version (algoliasearch/lite) is under 5kB and covers 99% of frontend use cases by omitting the indexing methods. In your nuxt.config.ts, setting lite: true in the Algolia module config switches to the lite client automatically.
The second concern is caching. Algolia’s JavaScript client includes a built-in in-memory response cache, but it resets on every page load. For Nuxt applications with SSR, you can add an HTTP-level cache at the route level using Nuxt’s routeRules. For search result pages with predictable queries — like category pages — a short cache of 60 seconds dramatically reduces Algolia operation consumption without meaningfully impacting data freshness:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/products/**': { swr: 60 }, // Stale-while-revalidate, 60 seconds
'/blog': { swr: 300 }, // 5 minutes for blog listing
'/search': { ssr: true } // Always fresh for search page
}
})
Finally, connect Algolia’s Click Analytics and Conversion tracking before launch. These aren’t just vanity metrics — they feed the Algolia A/B testing and ranking tuning features. A web app search system without analytics is essentially flying blind: you have no way to know whether users are finding what they’re looking for, which queries return zero results, or whether your custom ranking rules are actually helping. The @nuxtjs/algolia module’s Insights option adds event tracking with minimal configuration, and it’s one of those things that’s much easier to add before you have thousands of users than after.
FAQ
useAlgoliaSearch and useAsyncAlgoliaSearch in Nuxt 3?
useAlgoliaSearch is a client-side composable that returns a reactive search function and a result ref — use it for interactive, user-driven search (search bars, live filtering). useAsyncAlgoliaSearch wraps the query inside Nuxt’s useAsyncData, runs on the server during SSR, and serializes results into the page payload. Use it when search results need to be in the initial HTML for SEO or to avoid a content flash on page load. If you confuse them, your app still works — you just either waste server resources on interactive search or lose SEO value on content pages.
Yes, with one important caveat. The @nuxtjs/algolia module handles the SSR lifecycle correctly, and useAsyncAlgoliaSearch is fully SSR-compatible. However, Vue InstantSearch components rely on browser APIs and must be wrapped in <ClientOnly> or loaded as a .client.ts plugin to prevent hydration mismatches. The recommended pattern is to use useAsyncAlgoliaSearch for initial page data and Vue InstantSearch for the interactive UI layer, with a fallback skeleton shown during client hydration.
Three steps. First, go to the Algolia dashboard → your index → Configuration → Facets, and add the attributes you want to filter by (e.g., category, brand, price_range) as facet attributes. Second, in your Nuxt component, set up Vue InstantSearch inside a <ClientOnly> wrapper with <AisInstantSearch> as the provider. Third, add <AisRefinementList attribute="category" /> for each facet attribute inside the provider. The components handle state, URL sync, and API calls automatically — you just style them.
