Skip to content
SearchJet
Vue & Nuxt Integration

Instant Search for Vue 3 & Nuxt — Composables & SSR Ready

AI-Powered Search for Vue and Nuxt Applications

Add AI-powered search to Vue/Nuxt apps in minutes. One HTTP request, a small composable you own, Nuxt server routes for SSR.

The Problem

Vue Search is Fragmented

The Vue ecosystem lacks a cohesive search solution. You're forced to choose between abandoned libraries, vendor lock-in, or building everything yourself.

⚠

No Official Vue Search Lib

The Vue ecosystem lacks a first-party search solution. You're left stitching together community libraries or building from scratch.

⚠

Algolia Vue = Locked In

Vue InstantSearch ties your search UI to Algolia's ecosystem, with no standalone option.

⚠

Custom Meilisearch = Boilerplate

Manual API integration with Meilisearch requires writing your own reactive composables, state management, and error handling.

The Solution

SearchJet + Vue & Nuxt

No SDK needed — a reactive composable you own and Nuxt 3 server routes for SSR, rendered with your own components.

A Composable You Own

A ~30-line useSearchJet composable (ref + watch + fetch) with full Vue 3 reactivity. Results update automatically when the query changes.

Nuxt 3 Server Routes

A server/api/search.get.ts route keeps your key in runtimeConfig. Pair it with useAsyncData to pre-fetch results server-side for SEO-critical pages.

Your Components, Your UI

Results are plain JSON (id, title, content, url, excerpt), so you render them with your own components, slots, and styles.

TypeScript Friendly

No SDK types to learn. Describe the response (results, total, limit, offset) with a small interface and Volar infers it in templates.

Pinia / Vuex Ready

Works with your state management. Call the search API from Pinia actions or Vuex modules, or use your composable inside a setup store.

i18n Compatible

Works with @nuxtjs/i18n and vue-i18n. Build a multi-language search UI with your own translated strings.

Copy-Paste Integration

Two approaches — Nuxt 3 server route + page, or a reusable composable

Nuxt 3 Server Route + Page with useAsyncData

server/api/search.get.ts + pages/search.vue
// server/api/search.get.ts — runs on the server, so the key stays private
// nuxt.config.ts: runtimeConfig: { searchjetSearchKey: '' }  (env: NUXT_SEARCHJET_SEARCH_KEY)
export default defineEventHandler(async (event) => {
  const { q } = getQuery(event);
  if (!q) return { results: [], total: 0 };
  const { searchjetSearchKey } = useRuntimeConfig(event);
  return $fetch('https://app.searchjetengine.com/api/v1/search', {
    query: { q, limit: 10 },
    headers: { Authorization: `Bearer ${searchjetSearchKey}` },
  });
});

<!-- pages/search.vue -->
<script setup>
const route = useRoute();
const query = ref(String(route.query.q ?? ''));

// Rendered on the server on first load, re-fetched when query changes
const { data, status } = await useAsyncData(
  'search',
  () => $fetch('/api/search', { query: { q: query.value } }),
  { watch: [query] }
);
</script>

<template>
  <div class="max-w-4xl mx-auto py-12 px-6">
    <!-- .lazy = search on Enter/blur; use a debounced ref for search-as-you-type -->
    <input
      v-model.lazy="query"
      type="search"
      placeholder="Search docs..."
      class="w-full p-4 border-2 border-gray-200 rounded-xl text-lg focus:border-blue-500 focus:outline-none"
    />

    <div v-if="status === 'pending'" class="mt-8 text-center text-gray-500">
      Searching...
    </div>

    <div v-else class="mt-8 space-y-4">
      <p v-if="query">{{ data?.total ?? 0 }} results</p>
      <article
        v-for="hit in data?.results ?? []"
        :key="hit.id"
        class="p-6 border border-gray-200 rounded-xl hover:border-blue-300"
      >
        <h3 class="text-xl font-semibold"><a :href="hit.url">{{ hit.title }}</a></h3>
        <p class="mt-2 text-gray-600">{{ hit.excerpt }}</p>
      </article>
    </div>
  </div>
</template>

Vue 3 Component with Composables

composables/useSearchJet.js + useProductSearch.js
// composables/useSearchJet.js — ~30 lines you write yourself (not a package)
import { ref, watch, toValue } from 'vue';

const API = 'https://app.searchjetengine.com/api/v1/search';
const SEARCH_KEY = 'your_search_api_key'; // search-only key; never the private key

export function useSearchJet(query, filters) {
  const hits = ref([]);
  const total = ref(0);
  const isSearching = ref(false);
  let controller, timer;

  watch(() => [toValue(query), toValue(filters)], ([q, f]) => {
    clearTimeout(timer);
    controller?.abort();
    if (!q) { hits.value = []; total.value = 0; return; }
    timer = setTimeout(async () => {
      controller = new AbortController();
      isSearching.value = true;
      const params = new URLSearchParams({ q, limit: '20' });
      if (f) params.set('filters', f);
      try {
        const res = await fetch(`${API}?${params}`, {
          headers: { Authorization: `Bearer ${SEARCH_KEY}` },
          signal: controller.signal,
        });
        const data = await res.json();
        hits.value = data.results ?? []; // results is the array of hits
        total.value = data.total ?? 0;
      } catch {} // aborted or network error
      isSearching.value = false;
    }, 200); // debounce
  }, { immediate: true });

  return { hits, total, isSearching };
}

// composables/useProductSearch.js
import { ref, computed } from 'vue';
import { useSearchJet } from './useSearchJet';

export function useProductSearch() {
  const query = ref('');
  const category = ref('');

  // "filters" only works on attributes marked filterable in your index
  // settings; otherwise the API returns error code INVALID_FILTER.
  const filters = computed(() =>
    category.value ? `category = "${category.value}"` : ''
  );

  const { hits, total, isSearching } = useSearchJet(query, filters);

  // price and category are custom fields you indexed
  const products = computed(() =>
    hits.value.map((hit) => ({
      ...hit,
      formattedPrice: new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(hit.price),
    }))
  );

  return { query, category, products, total, isSearching };
}

Need a complete starter project?

View GitHub Examples
Developer Productivity

Measurable Developer Experience Gains

Illustrative example of a Vue or Nuxt app adding hosted search. Your results will vary.

MetricBeforeAfterImpact
Development TimeWeeksDaysMuch faster to launch
Bundle Size45KB+ (Algolia)None — built-in fetchNo SDK to ship
Search LatencySlower on large datasets<150msFaster results
Monthly CostUsage-based (Algolia)$0-19 (Free or Pro)A fraction of the cost

Frequently Asked Questions

Everything you need to know about SearchJet for Vue/Nuxt

Does SearchJet work with Nuxt 3?
Yes. Add a Nuxt server route (server/api/search.get.ts) that calls the SearchJet REST API with the key from useRuntimeConfig, then load it with useAsyncData or useFetch for SSR and pre-rendered search results.
What Vue versions are supported?
The sample composable uses the Vue 3 Composition API (toValue needs Vue 3.3+). Because SearchJet is a plain REST API, any app that can make an HTTP request can call it, including Vue 2.
Are there Vue composables?
There is no SDK to install. Copy the small useSearchJet composable below (ref + watch + fetch) and extend it with filters, sorting, or pagination. You own the code.
Does it work with Pinia?
Yes. Call the search API from Pinia actions or Vuex modules, or use your useSearchJet composable inside a setup store.
Is there a pre-built component library?
No. The API returns plain JSON, so you render results with your own components and slots. The examples below show a complete search page in a few lines of template.
How does it handle i18n?
Use @nuxtjs/i18n or vue-i18n for your search UI text. The search API is called the same way in every locale.

Ready to Add Search to Your Vue/Nuxt App?

Join developers building exceptional search experiences with SearchJet

Already have an account? Log in

Related Use Cases