How to Integrate Typesense with Astro: Complete Guide
Step-by-step guide to integrating Typesense with your Astro website.
Typesense is an open-source, typo-tolerant search engine designed for instant search experiences. It is fast (single-digit millisecond latency), easy to set up, and works well for content-heavy sites. What sets Typesense apart from other search engines is its strict schema enforcement and built-in API key scoping, which gives you more control over what users can search and access.
For Astro blogs and documentation sites, Typesense provides a solid middle ground between basic client-side search and expensive hosted services. You can self-host it for free or use Typesense Cloud for a managed experience.
Prerequisites
- Node.js 22.12.0 or newer. Astro 6 declares
node >=22.12.0in its engines field, and odd-numbered releases like Node 23 are not supported, so an even LTS line such as Node 22 or Node 24 is the safe target. - An Astro project (
npm create astro@latest). At the time of writing the current line is Astro 6.4.2. - Typesense server running locally or on Typesense Cloud
- Docker (recommended for local development)
A Note on Astro Rendering Modes
The search box you build in this guide runs entirely in the browser. It calls Typesense directly from the client using a search-only API key, so it works on a plain static Astro site. The default output in Astro 6 is 'static', and you do not need an adapter for client-side search.
If you instead proxy search through an Astro API route (to hide the search key, add rate limiting, or do server-side rendering of results), that route must be rendered on demand. In Astro 6 you opt a single route in with export const prerender = false and add an adapter such as @astrojs/node. Note that the old output: 'hybrid' mode was removed in Astro 5.0. The static and hybrid modes were merged into a single 'static' mode that prerenders by default and lets you render individual routes on demand once an adapter is installed. If you are upgrading an older project, delete any output: 'hybrid' line from astro.config.mjs.
Running Typesense
Start Typesense locally with Docker. The current stable server release is v30.2:
docker run -d \
-p 8108:8108 \
-v $(pwd)/typesense-data:/data \
typesense/typesense:30.2 \
--data-dir /data \
--api-key=your_api_key_here \
--enable-cors
Typesense is now running at http://localhost:8108.
Installation
Install the Typesense client and the InstantSearch adapter. Both packages list @babel/runtime as a required companion install, so include it explicitly:
npm install typesense@3.0.6 typesense-instantsearch-adapter@3.0.2 @babel/runtime
The adapter lets you use Algolia's InstantSearch UI components with Typesense as the backend. It works with instantsearch.js 4.51 or newer, react-instantsearch 6.39 or newer, and a Typesense server v0.25.0 or newer.
Configuration
Add your Typesense credentials to .env:
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=your_api_key_here
PUBLIC_TYPESENSE_HOST=localhost
PUBLIC_TYPESENSE_PORT=8108
PUBLIC_TYPESENSE_PROTOCOL=http
PUBLIC_TYPESENSE_SEARCH_KEY=your_search_only_key
Create the Typesense client:
// src/lib/typesense.ts
import Typesense from "typesense";
// Admin client (server-side, for indexing)
export const adminClient = new Typesense.Client({
nodes: [
{
host: import.meta.env.TYPESENSE_HOST || "localhost",
port: parseInt(import.meta.env.TYPESENSE_PORT || "8108"),
protocol: import.meta.env.TYPESENSE_PROTOCOL || "http",
},
],
apiKey: import.meta.env.TYPESENSE_API_KEY,
connectionTimeoutSeconds: 5,
});
Creating the Collection Schema
Typesense requires you to define a schema before indexing. This is different from Meilisearch or Elasticsearch, which can infer schemas:
// scripts/create-typesense-schema.ts
import Typesense from "typesense";
const client = new Typesense.Client({
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
apiKey: process.env.TYPESENSE_API_KEY || "your_api_key_here",
});
const schema = {
name: "posts",
fields: [
{ name: "title", type: "string" as const },
{ name: "description", type: "string" as const },
{ name: "content", type: "string" as const },
{ name: "slug", type: "string" as const, facet: false },
{ name: "category", type: "string" as const, facet: true },
{ name: "tags", type: "string[]" as const, facet: true },
{ name: "author", type: "string" as const, facet: true },
{ name: "publishDate", type: "int64" as const, sort: true },
],
default_sorting_field: "publishDate",
};
try {
await client.collections("posts").delete();
console.log("Deleted existing collection");
} catch (e) {
// Collection does not exist yet
}
await client.collections().create(schema);
console.log("Created posts collection");
Indexing Content
// scripts/index-to-typesense.ts
import Typesense from "typesense";
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const client = new Typesense.Client({
nodes: [{ host: "localhost", port: 8108, protocol: "http" }],
apiKey: process.env.TYPESENSE_API_KEY || "your_api_key_here",
});
const postsDir = path.join(process.cwd(), "public/data/posts");
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".mdx"));
const documents = files.map((file) => {
const raw = fs.readFileSync(path.join(postsDir, file), "utf-8");
const { data, content } = matter(raw);
return {
id: file.replace(".mdx", ""),
title: data.title || "",
description: data.description || "",
content: content.slice(0, 10000),
slug: file.replace(".mdx", ""),
category: data.category || "Uncategorized",
tags: data.tags || [],
author: data.author || "unknown",
publishDate: Math.floor(new Date(data.publishDate).getTime() / 1000),
};
});
const results = await client
.collections("posts")
.documents()
.import(documents, { action: "upsert" });
const success = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
console.log("Indexed: " + success + " success, " + failed + " failed");
Building the Search UI
Create a search component using vanilla JavaScript with safe DOM methods:
---
// src/components/TypesenseSearch.astro
---
<div id="search-wrapper">
<input type="text" id="ts-search-input" placeholder="Search articles..." autocomplete="off" />
<ul id="ts-search-results"></ul>
</div>
<script>
import Typesense from "typesense";
const client = new Typesense.Client({
nodes: [
{
host: import.meta.env.PUBLIC_TYPESENSE_HOST,
port: parseInt(import.meta.env.PUBLIC_TYPESENSE_PORT),
protocol: import.meta.env.PUBLIC_TYPESENSE_PROTOCOL,
},
],
apiKey: import.meta.env.PUBLIC_TYPESENSE_SEARCH_KEY,
connectionTimeoutSeconds: 5,
});
const input = document.getElementById("ts-search-input") as HTMLInputElement;
const resultsList = document.getElementById("ts-search-results") as HTMLUListElement;
let timer: ReturnType<typeof setTimeout>;
input.addEventListener("input", () => {
clearTimeout(timer);
timer = setTimeout(async () => {
const query = input.value.trim();
if (!query) {
resultsList.replaceChildren();
return;
}
const res = await client.collections("posts").documents().search({
q: query,
query_by: "title,description,content,tags",
query_by_weights: "4,3,1,2",
per_page: 10,
});
resultsList.replaceChildren();
(res.hits || []).forEach((hit: any) => {
const doc = hit.document;
const li = document.createElement("li");
const link = document.createElement("a");
link.href = "/blog/" + doc.slug;
const title = document.createElement("h3");
title.textContent = doc.title;
const desc = document.createElement("p");
desc.textContent = doc.description;
link.appendChild(title);
link.appendChild(desc);
li.appendChild(link);
resultsList.appendChild(li);
});
}, 200);
});
</script>
The query_by_weights parameter controls relevance. In this example, title matches rank highest (4), followed by description (3), tags (2), and content (1).
Scoped API Keys
Typesense lets you create scoped API keys that restrict what a user can search. This is useful for multi-tenant applications:
const scopedKey = adminClient.keys().generateScopedSearchKey(
"your_search_only_key",
{
filter_by: "category:=Tutorials",
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour
}
);
Production Tips
Use JSONL import for large datasets. Typesense supports bulk imports via JSONL format. For thousands of documents, this is significantly faster than individual inserts.
Set up synonyms. Configure synonyms so "JS" matches "JavaScript" and "k8s" matches "Kubernetes". This improves search quality without requiring users to know exact terms.
Enable analytics. Typesense Cloud provides search analytics. For self-hosted, log queries and click-through rates yourself to identify gaps in your content.
Use geo-search if applicable. Typesense supports geographic search. If your content is location-based, add a
geopointfield to sort results by proximity.
Scale with replicas. For high-traffic sites, run Typesense in a cluster with 3 or 5 nodes for high availability. The raft consensus protocol handles leader election automatically.
Alternatives to Consider
- Meilisearch if you prefer a more forgiving setup without strict schemas and want schemaless indexing.
- Algolia if you want a fully managed service and your search volume fits within the free tier.
- Pagefind if your site is fully static and you want zero-dependency, build-time search.
Common Errors and Fixes
Cannot find module '@babel/runtime/...' at build or runtime. Both typesense and typesense-instantsearch-adapter declare @babel/runtime as a peer install. If your package manager does not auto-install peers, the bundle breaks. Install it explicitly with npm install @babel/runtime.
Search calls fail with a 401 in the browser. This usually means you are using the admin (master) API key on the client. The Typesense docs are explicit that browser code must use a search-only key, never the master key. Generate a key scoped to search actions on a specific collection and expose only that one through a PUBLIC_ environment variable.
CORS errors when calling Typesense from the browser. The server only sends CORS headers when started with --enable-cors. The Docker command above includes it. On Typesense Cloud, enable CORS for your cluster in the dashboard.
output: 'hybrid' is not a valid option after upgrading Astro. The hybrid output mode was removed in Astro 5.0 and folded into the default 'static' mode. Remove the line from astro.config.mjs. To render a route on demand, add an adapter and set export const prerender = false on that route.
prerender does not respond to an environment variable. Astro 5.0 dropped support for dynamic values in prerender exports. Only the literal values true and false are accepted now, so compute the decision at build time or split the route.
Import returns partial failures. documents().import(...) does not throw on per-document errors. It returns an array where each entry has a success flag. Always inspect that array (as the indexing script does) rather than assuming the whole batch landed.
default_sorting_field rejected. The field named in default_sorting_field must exist in the schema and be a numeric type (int32, int64, or float) with sort allowed. The schema above stores publishDate as an int64 Unix timestamp for this reason. Do not point it at a string field.
Official Docs and Examples
- Typesense official documentation: https://typesense.org/docs/
- Typesense JavaScript and TypeScript client (install, configuration, and
doc/examples/server/keys.jsfor generating a search-only key): https://github.com/typesense/typesense-js - Typesense InstantSearch adapter (install, compatibility table, and a working
src/example app): https://github.com/typesense/typesense-instantsearch-adapter - Managing access to data and scoped search keys: https://typesense.org/docs/guide/data-access-control.html
- Astro on-demand rendering and adapters: https://docs.astro.build/en/guides/on-demand-rendering/
- Astro v5 upgrade guide (the
output: 'hybrid'removal andprerenderchanges): https://docs.astro.build/en/guides/upgrade-to/v5/
Wrapping Up
Typesense brings fast, typo-tolerant search to Astro sites with more control than most alternatives. The strict schema enforcement catches data issues early, the scoped API keys provide fine-grained access control, and the InstantSearch adapter means you get a polished UI with minimal effort. Whether you self-host or use Typesense Cloud, the search experience your visitors get will be fast, relevant, and forgiving of typos.
Sources
Checked on 2026-05-29.
- typesense on npm (latest 3.0.6)
- typesense-instantsearch-adapter on npm (latest 3.0.2)
- astro on npm (latest 6.4.2)
- Typesense JavaScript client README (install command,
@babel/runtime, search-only key guidance) - Typesense InstantSearch adapter README (install, version, peer dependency table, init example)
- Typesense Docker Hub tags (stable 30.2 confirmed)
- Typesense documentation home (current server line v30.2)
- Typesense data access control and scoped search keys
- Astro on-demand rendering guide (static default, adapter requirement, prerender export)
- Astro v5 upgrade guide (
output: 'hybrid'removed, dynamicprerenderremoved)
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.