A Nuxt module for integrating Typesense - a fast, typo-tolerant search engine - into your Nuxt 4 application.
📖 Read the full documentation for detailed guides, API references, and examples.
Try the playground at https://nuxt-typesense.netlify.app/. The public demo is read-only (search, collections and examples). The write demos (tuning, curation, operations, keys, ...) are hidden there, so run the playground against your own Typesense instance to use them, see Try the playground.
- 🚀 Full Typesense API Support - TypeScript API client generated from the Typesense OpenAPI spec
- 🔒 Server mode by default - API key stays on the server, the module registers
/api/typesense/*proxy routes - 🖥️ Optional client mode - auto-imported composables that talk to Typesense directly from the browser
- ⚡️ Type Safety - Full TypeScript support, including typed
runtimeConfig.typesense - 🎨 SSR Compatible - Works with Nuxt's SSR
- Nuxt
^4.x - Node.js
^22.22.3 || ^24.15.0(the versions Nuxt 4.6 supports)
- Add the module to your project:
pnpm add @sfxcode/nuxt-typesense
# or: yarn add @sfxcode/nuxt-typesense
# or: npm install @sfxcode/nuxt-typesense- Add it to the
modulessection ofnuxt.config.ts:
export default defineNuxtConfig({
modules: ['@sfxcode/nuxt-typesense'],
typesense: {
url: process.env.TYPESENSE_URL, // e.g. http://localhost:8108
apiKey: process.env.TYPESENSE_API_KEY,
},
})# .env
TYPESENSE_URL=http://localhost:8108
TYPESENSE_API_KEY=your-api-key-hereThat's it ✨ The module logs a warning at startup if url or apiKey is missing.
interface ModuleOptions {
url?: string // Typesense server URL
apiKey?: string // Typesense API key
clientMode?: boolean // default: false
routes?: { readOnly?: boolean, exclude?: string[] } // limit the proxy routes (server mode)
openAPI?: boolean | { path?: string, title?: string, version?: string, description?: string } // serve /api/typesense/openapi.json (server mode)
}The two modes decide where the key lives and what is available:
Server mode (clientMode: false, default) |
Client mode (clientMode: true) |
|
|---|---|---|
| Key stored in | private runtimeConfig.typesense |
public runtimeConfig.public.typesense |
Server routes /api/typesense/* |
✅ registered | ✅ registered |
Server util useTypesenseServerApi(url, apiKey) |
✅ auto-imported in Nitro | ✅ auto-imported in Nitro |
Composables (useTypesenseApi, …) |
❌ not available | ✅ auto-imported |
⚠️ Important:clientModechanges where the key is exposed. In browser mode the key is shipped to every visitor, so use only a search-only API key there. Keep admin or write-capable keys in server mode and use the registered/api/typesense/*proxy routes oruseTypesenseServerApi()for private operations.At build time the module throws if
clientMode: trueuses anapiKeyequal toTYPESENSE_ADMIN_KEYorNUXT_TYPESENSE_ADMIN_KEY. Other ways of supplying an admin key are not detected.
Use the registered routes from components (useFetch) or other server code:
<script setup lang="ts">
const { data: status } = await useFetch('/api/typesense/status')
const { data: collections } = await useFetch('/api/typesense/collections')
</script>Search a collection (the body is the search parameters):
const results = await $fetch('/api/typesense/documents/products/search', {
method: 'POST',
body: { q: 'laptop', query_by: 'name,description', per_page: 20 },
})Routes registered by the module:
| Area | Routes |
|---|---|
| Status | GET /api/typesense/status |
| Collections | GET/POST /api/typesense/collections, GET/PATCH/DELETE /api/typesense/collections/:name |
| Aliases | GET/PUT/DELETE /api/typesense/alias/:alias |
| Documents | POST /api/typesense/documents/:collection plus /search, /multi-search, /export, /import, /delete, /update, and /id/:id (GET/PATCH/DELETE) |
| API keys | GET/POST /api/typesense/keys, GET/DELETE /api/typesense/keys/:id, POST /api/typesense/key/scoped/:key |
See Server Routes for request and response details.
useTypesenseServerApi is auto-imported in Nitro and returns all generated API clients:
// server/api/products/search.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event)
const { documentsApi } = useTypesenseServerApi(config.typesense.url, config.typesense.apiKey)
const { q } = getQuery(event)
return await documentsApi.searchCollection({
collectionName: 'products',
searchParameters: { q: String(q ?? '*'), queryBy: 'name,description' },
})
})// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@sfxcode/nuxt-typesense'],
typesense: {
url: process.env.TYPESENSE_URL,
apiKey: process.env.TYPESENSE_SEARCH_KEY, // search-only key!
clientMode: true,
},
})<script setup lang="ts">
const { searchCollection } = useTypesenseDocuments()
const { collectionInfos } = useTypesenseCollections()
const results = await searchCollection('products', { q: 'laptop', queryBy: 'name' })
const collections = await collectionInfos()
// Or use the full API client set
const { documentsApi, healthApi } = useTypesenseApi()
// Access configuration
const url = useTypesenseUrl()
const apiKey = useTypesenseApiKey()
</script>Available composables: useTypesenseApi, useTypesenseDocuments, useTypesenseCollections, useTypesenseUrl, useTypesenseApiKey.
useTypesenseApi() (client) and useTypesenseServerApi(url, apiKey) (server) both return:
{
analyticsApi, // Analytics and event tracking
collectionsApi, // Collection management
conversationsApi, // Conversation models
curationSetsApi, // Curation sets
debugApi, // Debug operations
documentsApi, // Document CRUD and search
healthApi, // Health check
keysApi, // API key management
searchModelsApi, // NL search models
operationsApi, // Cluster operations
presetsApi, // Search presets
stemmingApi, // Stemming dictionaries
stopwordsApi, // Stopwords
synonymsApi, // Synonyms
}Note that the generated clients use camelCase parameter names (queryBy, filterBy, perPage), not Typesense's snake_case. The proxy routes forward the request body as search parameters, so use the same camelCase names there.
| ✅ Do | ❌ Don't |
|---|---|
| Use server mode with an admin or scoped key | Put an admin key in clientMode |
| Use search-only keys for client mode | Hardcode API keys in source code |
| Create scoped keys per user/tenant | Share API keys between environments |
| Rotate keys regularly | Commit keys to version control |
🔒 Security Rule: If it modifies data, it belongs on the server.
The proxy routes use the server key and have no built-in authentication. Limit what is exposed with the routes option, and protect /api/typesense/** with your own middleware on public sites:
typesense: {
routes: {
readOnly: true, // GET routes, search and multi-search only (no key listing or debug)
exclude: ['/api/typesense/operations'], // drop route groups by prefix
},
}See Configuration for details.
git clone https://github.com/sfxcode/nuxt-typesense.git
cd nuxt-typesense
pnpm install # pnpm 12 (see "packageManager" in package.json)
pnpm run dev:prepare # generate type stubs
pnpm run dev # start the playgroundThe playground talks to whichever Typesense instance TYPESENSE_URL and TYPESENSE_API_KEY point to.
- Start a Typesense server, for example with Docker (skip this if you already have one):
docker run -d --name typesense -p 8108:8108 -v typesense-data:/data \
typesense/typesense:30.2 --data-dir /data --api-key=xyz --enable-cors- Copy the example env file and edit it:
cp playground/.env.example playground/.env# playground/.env
TYPESENSE_URL=http://localhost:8108 # your server, no trailing slash
TYPESENSE_API_KEY=xyz # an admin key for local use- Run the playground and open http://localhost:3000:
pnpm run devNotes:
playground/.envis git-ignored. Use an admin key locally so every page works, including tuning, curation, operations and key management. Never use an admin key on a public deploy.- You can also set the variables inline:
TYPESENSE_URL=https://search.example.com TYPESENSE_API_KEY=... pnpm run dev. - A server behind a reverse proxy usually exposes plain HTTPS on port 443, so use
https://hostwithout:8108. Port 8108 speaks plain HTTP only. - A fresh server has no collections. Create one on the Collections page, or via
POST /api/typesense/collections, before using Search. - Browser-side calls to a remote server need CORS enabled on it (
--enable-cors). Server mode, the default, is not affected because the Nuxt server makes the requests.
The Netlify build (netlify.toml) sets NITRO_PRESET=netlify, which switches the playground into public-demo mode: only read routes are registered, the OpenAPI route and the write demo pages are removed, and security headers are added. Set TYPESENSE_URL and a search-only key (actions documents:search, documents:get, collections:get, collections:list) in the Netlify site environment.
pnpm run dev # run the playground
pnpm run dev:build # build the playground
pnpm run prepack # build the module
pnpm run test # run tests (vitest)
pnpm run test:watch # tests in watch mode
pnpm run lint # eslint
pnpm run api-codegen # regenerate the API client, then apply patches
pnpm run release # lint, test, build, version, publish
pnpm run docs:dev # documentation dev serverFor a full type check, run npx nuxi typecheck playground.
nuxt-typesense/
├── src/
│ ├── module.ts # Module definition and route registration
│ ├── types.ts # Shared types
│ └── runtime/
│ ├── api/ # Generated Typesense API client (do not edit by hand)
│ ├── composables/ # Client-mode composables
│ ├── server/
│ │ ├── api/typesense/ # Server route handlers
│ │ └── composables/ # useTypesenseServerApi
│ ├── model/ # TypeScript models
│ └── utils/ # Helpers (removeUndefined)
├── scripts/patch-api.mjs # Fixes applied after api-codegen
├── playground/ # Development playground app
├── docs/ # VitePress documentation
└── test/ # Vitest tests
pnpm run api-codegenThis fetches the latest Typesense OpenAPI spec, regenerates src/runtime/api, and then runs scripts/patch-api.mjs. The generator output needs a few manual fixes, and the script applies them (it is safe to run repeatedly):
DocumentsApi: sendsearchParametersandmultiSearchParametersas query parameters and dropundefinedvalues (removeUndefined)runtime.ts: add theoverridemodifier onFetchError.causeCreateAnalyticsRuleRequest: remove a duplicated importCreateAnalyticsRule200ResponseOneOfInner: re-add the missinginstanceOf…type guard
If the generator output changes so a patch no longer matches, the script fails with the missing snippet, so you can update it. Afterwards run pnpm run lint, pnpm run test and npx nuxi typecheck playground.
Full documentation is published at https://sfxcode.github.io/nuxt-typesense/ (source in the docs/ directory: Getting Started, Configuration, Core Concepts, API Reference, Examples).
pnpm run docs:dev # dev server (http://localhost:5173)
pnpm run docs:build
pnpm run docs:previewContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- 📖 Nuxt Typesense Documentation
- 📖 Typesense Documentation
- 🎮 Typesense API Explorer
- 💬 Typesense Slack Community
- 🐙 GitHub Repository
MIT License © 2024 sfxcode