
- Typesense is an open-source search engine built for fast, typo-tolerant search.
- Your primary database should remain the source of truth; Typesense stores a searchable copy of the required data.
- A collection is similar to a database table, while documents are similar to rows.
- The official JavaScript client can be installed using
npm install typesense.- Use a write-enabled API key only on the backend.
- Use a search-only or scoped key for requests made from browsers and mobile applications.
- Fields must be declared correctly in the collection schema before they can be searched, filtered, faceted, or sorted.
- Keep Typesense synchronised whenever records are created, updated, or deleted in the primary database.
Search looks simple until real users begin typing incomplete product names, spelling words incorrectly, combining filters, and expecting useful results instantly. A basic database query may work initially, but it becomes difficult to manage once the application needs typo tolerance, relevance ranking, faceting, and search-as-you-type.
That is what led me to explore Typesense for Node.js applications. It provides the search capabilities users expect without requiring every behaviour to be built from scratch. In this guide, I’ll show you how to run Typesense, connect it to Node.js, create a collection, index documents, and implement practical searches, filters, facets, sorting, and pagination.
What Is Typesense?
Typesense is an open-source search engine designed for fast, relevant, and typo-tolerant search. Applications send searchable documents to Typesense and query them through its API.
It can be self-hosted or used through Typesense Cloud. Typesense is commonly added to e-commerce stores, directories, documentation websites, content platforms, and applications that need search-as-you-type experiences.
How Does Typesense Work With Node.js?
Typesense does not normally replace your application database. PostgreSQL, MySQL, MongoDB, or another database should continue to store the authoritative version of your data.
The Node.js application sends selected fields from that data to Typesense. These fields are indexed so that Typesense can search them efficiently.
A typical request flow looks like this:
- A product is created or updated in the primary database.
- The Node.js backend sends the searchable product fields to Typesense.
- A user enters a query in the application.
- The application sends the query to Typesense.
- Typesense searches its index and returns matching document IDs and fields.
- The application displays the results or retrieves additional information from the primary database.
This separation matters. Typesense is responsible for discovery and search relevance, while the database remains responsible for durable application data and business transactions.
Why Use Typesense?
1. Typo-Tolerant Search
Typesense can return relevant results even when a query contains minor spelling mistakes. A search for hedphones, for example, can still find products containing headphones.
2. Fast Search Experiences
The engine is designed for low-latency searches, making it suitable for autocomplete and search-as-you-type interfaces.
3. Filtering and Faceting
Search results can be filtered by fields such as price, category, availability, brand, or location. Facets can also show users how many results belong to each category.
4. Relevance Control
Developers can control which fields are searched, assign different importance to them, sort results, and fine-tune typo tolerance and matching behaviour.
5. Deployment Flexibility
Typesense can be self-hosted using Docker, installed as a binary, or run through Typesense Cloud.
6. Simpler Search Setup
Compared with broader search and analytics platforms, Typesense focuses on application search and provides a relatively direct API and schema model.
Common Typesense Use Cases
Typesense can support several search-heavy application experiences.
E-commerce Search
Users can search product names and descriptions while filtering by category, brand, price, rating, colour, or availability.
Documentation Search
Knowledge bases and documentation websites can index page titles, headings, body content, tags, and categories.
Marketplace and Directory Search
Directories can combine keyword search with location, category, price, rating, or availability filters.
Media and Content Discovery
Applications can index articles, videos, podcasts, tags, descriptions, and transcripts to make large content libraries easier to explore.
Location-Based Search
Typesense supports geographical search for applications that need to find nearby stores, restaurants, service providers, or delivery locations.
How to Integrate Typesense With Node.js
We will build a simple product-search example. The collection will contain a product title, description, category, price, stock status, and popularity score.
Step 1: Run Typesense Locally
The easiest local setup is through Docker.
Create a directory for persistent Typesense data:
mkdir typesense-dataStart a Typesense server:
docker run -d \
--name typesense \
-p 8108:8108 \
-v "$(pwd)/typesense-data:/data" \
typesense/typesense:30.2 \
--data-dir /data \
--api-key=YOUR_ADMIN_API_KEY \
--enable-corsReplace YOUR_ADMIN_API_KEY with a strong secret value.
Typesense will now be available at:
http://localhost:8108For production environments, use a supported stable version, persistent storage, backups, monitoring, and high-availability configuration appropriate to your application.
If you do not want to manage the infrastructure, you can create a hosted cluster through Typesense Cloud.
Step 2: Create a Node.js Project
Create a new project and install the required packages:
mkdir typesense-node-demo
cd typesense-node-demo
npm init -y
npm install typesense dotenvCreate a .env file:
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_ADMIN_API_KEY=YOUR_ADMIN_API_KEYDo not commit this file to version control. Add it to .gitignore:
node_modules
.envStep 3: Initialise the Typesense Client
Create a file named typesenseClient.js:
require("dotenv").config();
const Typesense = require("typesense");
const client = new Typesense.Client({
nodes: [
{
host: process.env.TYPESENSE_HOST,
port: Number(process.env.TYPESENSE_PORT),
protocol: process.env.TYPESENSE_PROTOCOL,
},
],
apiKey: process.env.TYPESENSE_ADMIN_API_KEY,
connectionTimeoutSeconds: 2,
});
module.exports = client;The configuration contains:
host: The Typesense server or cloud-cluster hostnameport: The port on which Typesense is availableprotocol:httplocally orhttpsfor most hosted environmentsapiKey: The key used to authorise requestsconnectionTimeoutSeconds: How long the client waits before treating the connection as unavailable
A production configuration can include multiple nodes and a nearest node to improve availability and request routing.
Step 4: Check the Connection
Create checkConnection.js:
const client = require("./typesenseClient");
async function checkConnection() {
try {
const collections = await client.collections().retrieve();
console.log("Connected to Typesense");
console.log(collections);
} catch (error) {
console.error("Typesense connection failed:", error);
}
}
checkConnection();Run it:
node checkConnection.jsIf the connection succeeds, Typesense returns the available collections. A new instance will normally return an empty list.
If it fails, verify:
- The Typesense container is running.
- The host and port are correct.
- The API key matches the server key.
- The protocol is correct.
- Port
8108is accessible.
Step 5: Create a Collection
A collection contains related documents and defines how their fields should be indexed.
Create createCollection.js:
const client = require("./typesenseClient");
const productsSchema = {
name: "products",
fields: [
{
name: "title",
type: "string",
},
{
name: "description",
type: "string",
optional: true,
},
{
name: "category",
type: "string",
facet: true,
},
{
name: "price",
type: "float",
facet: true,
},
{
name: "in_stock",
type: "bool",
facet: true,
},
{
name: "popularity",
type: "int32",
},
],
default_sorting_field: "popularity",
};
async function createCollection() {
try {
const collection = await client.collections().create(productsSchema);
console.log("Collection created:", collection);
} catch (error) {
console.error("Unable to create collection:", error);
}
}
createCollection();Run the file:
node createCollection.jsThe schema tells Typesense how each field will be used:
stringis suitable for searchable text.floatstores prices or decimal values.boolstores true or false values.int32stores whole numbers.optional: trueallows documents to omit that field.facet: trueallows the field to be used for faceting and efficient filtering.default_sorting_fieldhelps rank equally relevant results using the popularity score.
Let’s Build Scalable Node.js Apps Together!
We build secure, high-performance Node.js backends that handle heavy traffic and scale with your business.
Typesense also supports automatic schema detection, but an explicit schema gives you clearer control in production applications.
Step 6: Retrieve, Update, and Delete Collections
Retrieve All Collections
const client = require("./typesenseClient");
async function getCollections() {
const collections = await client.collections().retrieve();
console.log(collections);
}
getCollections();Retrieve One Collection
const collection = await client.collections("products").retrieve();
console.log(collection);Add a Field to an Existing Collection
Typesense supports adding and removing fields through a schema update.
const updatedSchema = await client.collections("products").update({
fields: [
{
name: "brand",
type: "string",
facet: true,
optional: true,
},
],
});
console.log(updatedSchema);To remove an existing field, mark it for deletion:
await client.collections("products").update({
fields: [
{
name: "brand",
drop: true,
},
],
});Changing the type or configuration of an existing field may require removing and recreating it. For larger production collections, plan schema migrations carefully.
Delete a Collection
const deletedCollection = await client
.collections("products")
.delete();
console.log(deletedCollection);Deleting a collection removes its indexed documents. Use this operation carefully, especially in shared or production environments.
Step 7: Add Documents
A document is an individual searchable record inside a collection.
Add One Document
const client = require("./typesenseClient");
const product = {
id: "1",
title: "Wireless Laptop Mouse",
description: "Compact Bluetooth mouse for laptops and tablets",
category: "Accessories",
price: 29.99,
in_stock: true,
popularity: 85,
};
async function addProduct() {
const result = await client
.collections("products")
.documents()
.create(product);
console.log(result);
}
addProduct();Typesense accepts the id as a string. If it is omitted, Typesense can generate one automatically, but using your database record ID makes synchronisation easier.
Import Multiple Documents
const client = require("./typesenseClient");
const products = [
{
id: "1",
title: "Wireless Laptop Mouse",
description: "Compact Bluetooth mouse for laptops and tablets",
category: "Accessories",
price: 29.99,
in_stock: true,
popularity: 85,
},
{
id: "2",
title: "Gaming Laptop",
description: "High-performance laptop with dedicated graphics",
category: "Computers",
price: 1299.99,
in_stock: true,
popularity: 98,
},
{
id: "3",
title: "USB-C Phone Charger",
description: "Fast charger for phones and tablets",
category: "Accessories",
price: 19.99,
in_stock: false,
popularity: 72,
},
];
async function importProducts() {
const result = await client
.collections("products")
.documents()
.import(products, {
action: "upsert",
});
console.log(result);
}
importProducts();The upsert action creates a document when its ID does not exist and updates it when it does. This is useful when synchronising records from a database.
Bulk imports return an individual result for each document. Check the responses instead of assuming every record succeeded.
Step 8: Retrieve, Update, Upsert, and Delete Documents
Retrieve a Document
const product = await client
.collections("products")
.documents("1")
.retrieve();
console.log(product);Update a Document
const updatedProduct = await client
.collections("products")
.documents("1")
.update({
price: 24.99,
in_stock: false,
});
console.log(updatedProduct);The update operation changes only the supplied fields.
Upsert a Document
const product = await client
.collections("products")
.documents()
.upsert({
id: "1",
title: "Wireless Laptop Mouse",
description: "Compact Bluetooth mouse with silent buttons",
category: "Accessories",
price: 24.99,
in_stock: true,
popularity: 90,
});
console.log(product);Delete a Document
const deletedProduct = await client
.collections("products")
.documents("1")
.delete();
console.log(deletedProduct);When a record is deleted from the primary database, remove its corresponding Typesense document as well.
How to Search Documents in Typesense?
Perform a Simple Search
const results = await client
.collections("products")
.documents()
.search({
q: "laptop",
query_by: "title,description",
});
console.log(results.hits);The two important parameters are:
q: The text entered by the userquery_by: The fields Typesense should search
The order of fields in query_by matters. Here, a match in title is considered more relevant than a match in description.
query_by does not sort the response alphabetically. It tells Typesense which text fields should participate in the full-text search.
Search With Typo Tolerance
const results = await client
.collections("products")
.documents()
.search({
q: "gamng latop",
query_by: "title,description",
num_typos: 2,
});
console.log(results.hits);Typesense supports typo tolerance by default. The num_typos parameter lets you control the number of permitted typographical errors.
Allowing more typos is not always better. Excessive tolerance can produce irrelevant matches and require more processing.
Search Multiple Words
const results = await client
.collections("products")
.documents()
.search({
q: "wireless laptop",
query_by: "title,description",
});
console.log(results.hits);Typesense searches the individual query tokens and ranks documents based on factors such as matching fields, token proximity, typo count, and text match quality.
A multi-word query does not necessarily guarantee that every word must appear in every result. If you require an exact phrase, surround it with quotation marks:
const results = await client
.collections("products")
.documents()
.search({
q: '"wireless laptop"',
query_by: "title,description",
});Filtering Search Results
Filters narrow the result set using structured fields.
Filter by a Numeric Value
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
filter_by: "price:>500",
});
console.log(results.hits);The wildcard query q: "*" retrieves documents without requiring a text match. The filter then keeps products priced above 500.
Filter by a Numeric Range
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
filter_by: "price:>=20 && price:<=100",
});
console.log(results.hits);The && operator combines conditions that must both be true.
Filter by Category and Availability
const results = await client
.collections("products")
.documents()
.search({
q: "mouse",
query_by: "title,description",
filter_by: "category:=Accessories && in_stock:=true",
});
console.log(results.hits);The := operator performs an exact filter match.
Filter by Multiple Values
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
filter_by: "category:=[Accessories,Computers]",
});
console.log(results.hits);This returns products belonging to either selected category.
Filter by Document IDs
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
filter_by: "id:=[1,2]",
});
console.log(results.hits);Exclude a Value
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
filter_by: "id:!=1",
});
console.log(results.hits);This excludes the document with the ID 1.
Adding Facets
Facets group matching documents by a field and return the number of results in each group.
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
facet_by: "category,in_stock",
});
console.log(results.facet_counts);A response might show:
- Accessories: 2
- Computers: 1
- In stock: 2
- Out of stock: 1
The frontend can display these values as filters.
Faceting does not automatically filter the results. It provides grouped counts. The user’s selection must then be sent through filter_by.
For example:
const results = await client
.collections("products")
.documents()
.search({
q: "laptop",
query_by: "title,description",
facet_by: "category",
filter_by: "category:=Computers",
});Sorting Results
Use sort_by when results must follow a particular order.
Sort by Price
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
sort_by: "price:asc",
});
console.log(results.hits);Combine Relevance and Popularity
const results = await client
.collections("products")
.documents()
.search({
q: "laptop",
query_by: "title,description",
sort_by: "_text_match:desc,popularity:desc",
});
console.log(results.hits);This keeps text relevance as the primary ranking factor and uses popularity to break ties or improve the order among similarly relevant results.
Paginating Search Results
Typesense supports page-based pagination:
const results = await client
.collections("products")
.documents()
.search({
q: "*",
query_by: "title",
page: 2,
per_page: 10,
});
console.log(results.hits);The response also includes information such as the total number of matching documents and the processing time.
Do not request an unnecessarily large number of results in one response. Use pagination or an appropriate export workflow for larger datasets.
Putting Search Behind an Express API
Install Express:
npm install expressCreate server.js:
const express = require("express");
const client = require("./typesenseClient");
const app = express();
app.use(express.json());
app.get("/api/search/products", async (request, response) => {
try {
const {
q = "*",
category,
minPrice,
maxPrice,
page = 1,
} = request.query;
const filters = [];
if (category) {
filters.push(`category:=${category}`);
}
if (minPrice) {
filters.push(`price:>=${Number(minPrice)}`);
}
if (maxPrice) {
filters.push(`price:<=${Number(maxPrice)}`);
}
const searchParameters = {
q,
query_by: "title,description",
facet_by: "category,in_stock",
sort_by: "_text_match:desc,popularity:desc",
page: Number(page),
per_page: 10,
};
if (filters.length > 0) {
searchParameters.filter_by = filters.join(" && ");
}
const results = await client
.collections("products")
.documents()
.search(searchParameters);
response.json(results);
} catch (error) {
console.error("Search failed:", error);
response.status(500).json({
message: "Unable to search products",
});
}
});
app.listen(3000, () => {
console.log("Search API running at http://localhost:3000");
});Run the server:
node server.jsTry a request:
http://localhost:3000/api/search/products?q=laptop&category=Computers&minPrice=500In a real application, validate and escape all user-controlled filter values before constructing a Typesense filter expression. Do not pass unrestricted user input directly into filter_by or sort_by.
Let’s Build Scalable Node.js Apps Together!
We build secure, high-performance Node.js backends that handle heavy traffic and scale with your business.
How to Keep Typesense and Your Database in Sync
Typesense should reflect changes made to your primary database.
Update Both Within the Application
After a successful database write, add or update the corresponding Typesense document:
const savedProduct = await database.products.create(productData);
await client
.collections("products")
.documents()
.upsert({
id: String(savedProduct.id),
title: savedProduct.title,
description: savedProduct.description,
category: savedProduct.category,
price: savedProduct.price,
in_stock: savedProduct.inStock,
popularity: savedProduct.popularity,
});This is simple, but the database could succeed while the Typesense update fails.
Use a Queue or Background Worker
For more reliable production synchronisation:
- Save the data in the primary database.
- Publish an indexing job to a queue.
- Let a worker update Typesense.
- Retry failed jobs.
- Record indexing errors for investigation.
An outbox pattern can provide stronger guarantees by storing the data change and indexing event in the same database transaction.
Reindex Periodically
Even with event-driven updates, a scheduled reconciliation process can compare the database with Typesense and repair missing or outdated documents.
Securing Typesense in a Node.js Application
The bootstrap or admin API key can create collections, write documents, and manage other keys. Never expose it in browser code, mobile applications, public repositories, or client-visible environment variables.
Use separate keys for different responsibilities:
- Backend write key for indexing documents
- Search-only key for public search requests
- Scoped search key for user- or tenant-specific access
- Admin key for restricted operational tasks
A search-only key can be limited to the search action:
{
"description": "Search products only",
"actions": ["documents:search"],
"collections": ["products"]
}For a multi-tenant application, scoped search keys can enforce a filter such as:
tenant_id:=customer_42The filter is embedded in the key and cannot be removed by the client. This is safer than trusting the frontend to send the correct tenant filter.
Teams building large Node.js search integrations may also need support with schema planning, background synchronisation, API security, and scaling. In such cases, experienced Node.js developers can help design the search layer around the application’s actual data and traffic patterns.
Common Typesense Errors and Solutions
Connection Refused
Cause: Typesense is not running, or the configured host and port are incorrect.
Fix: Check the container status, port mapping, protocol, and environment variables.
docker psHTTP 401 or 403 Error
Cause: The API key is incorrect or does not have permission to perform the requested action.
Fix: Confirm that the backend uses a key with the required collection and action permissions.
Field Not Found in Schema
Cause: A document or query refers to a field that is not defined in the collection.
Fix: Add the field through a schema update or correct the field name. Ensure the document structure matches the schema.
Document Does Not Match the Schema
Cause: A value uses the wrong type, such as sending a string for a field declared as float.
Fix: Transform the data before indexing and inspect individual bulk-import results for failures.
Filtering or Faceting Does Not Work
Cause: The field has not been configured appropriately, or the filter syntax is incorrect.
Fix: Mark fields used for faceting withfacet: true, use the correct operators, and validate the expression against the relevant Typesense version.
Search Returns Unexpected Results
Cause: The wrong fields are listed inquery_by, typo tolerance is too broad, or ranking does not match the product’s needs.
Fix: Review field order, query weights, typo settings, filtering, and sorting. Test using real user queries rather than only ideal keywords.
Typesense Production Checklist
Before launching, confirm that:
- Typesense uses persistent storage.
- The admin key is stored only in a secret manager or secure backend environment.
- Browser requests use a search-only or scoped key.
- The primary database remains the source of truth.
- Indexing failures are retried and monitored.
- Bulk-import responses are inspected.
- Backups and recovery procedures have been tested.
- Search latency and error rates are monitored.
- High availability is configured when downtime would affect critical workflows.
- Collections can be rebuilt from the primary database.
- Search behaviour has been tested with real queries and spelling mistakes.
- Schema changes and reindexing are included in the deployment plan.
Frequently Asked Questions
Is Typesense Suitable for Production Node.js Applications?
Yes. Typesense supports typo-tolerant search, real-time indexing, filters, facets, sorting, geographical search, and high-availability deployments. Production readiness still depends on secure configuration, persistent storage, monitoring, backups, and reliable synchronisation.
Does Typesense Replace My Database?
No. Your database should remain the source of truth for application data and transactions. Typesense stores a searchable representation of selected fields and is optimised for retrieval, relevance, filtering, and discovery.
Is Typesense an Alternative to Elasticsearch?
Typesense can be an alternative when the primary requirement is application search with a simpler setup. Elasticsearch offers a broader search and analytics ecosystem but may require more operational knowledge and configuration.
Can I Use Typesense Directly From the Frontend?
Yes, but only with a search-only or scoped API key. Never expose an admin or write-enabled key in browser or mobile code, since it could allow unauthorised data or collection changes.
Does Typesense Support Typo Tolerance Automatically?
Yes. Typo tolerance is enabled by default for searchable text. Developers can control it through parameters such as num_typos and other matching settings when the default behaviour is too broad or restrictive.
Does Typesense Support Filters and Facets?
Yes. Filters restrict the returned documents, while facets group results and provide counts for selected fields. Fields used for faceting should be configured with facet: true in the collection schema.
Is Typesense Good for Multi-Tenant Applications?
Yes, provided access is designed correctly. Scoped search keys can enforce tenant-specific filters, preventing users from removing or overriding the access restriction included in their search credentials.
Our Final Words
Typesense works best when search is treated as a product feature rather than a basic database query. The goal is not simply to return records containing a word. It is to help users find the right product, document, or page even when their query is incomplete or misspelled.
What stood out while working through the Node.js integration was how quickly the basic pieces came together: define a schema, index the searchable fields, and send queries through the JavaScript client. The more important engineering work begins afterwards, tuning relevance, protecting API keys, synchronising database changes, and testing with real search behaviour.
Start with a small collection and a representative set of user queries. Once the results are useful and consistent, expand the schema, introduce filters and facets, and build a reliable synchronisation process for production.



