Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ This is a monorepo containing multiple standalone projects. Each project lives i

```plaintext
code-samples/
├── typesense-angular-search-bar/ # Angular + Typesense search implementation
├── typesense-astro-search/ # Astro + Typesense search implementation
├── typesense-gin-full-text-search/ # Go (Gin) + Typesense backend implementation
├── typesense-next-search-bar/ # Next.js + Typesense search implementation
├── typesense-nuxt-search-bar/ # Nuxt.js + Typesense search implementation
├── typesense-qwik-js-search/ # Qwik + Typesense search implementation
├── typesense-react-native-search-bar/ # React Native + Typesense search implementation
├── typesense-solid-js-search/ # SolidJS + Typesense search implementation
├── typesense-vanilla-js-search/ # Vanilla JS + Typesense search implementation
└── README.md # You are here
├── typesense-angular-search-bar/ # Angular + Typesense search implementation
├── typesense-astro-search/ # Astro + Typesense search implementation
├── typesense-gin-full-text-search/ # Go (Gin) + Typesense backend implementation
├── typesense-next-search-bar/ # Next.js + Typesense search implementation
├── typesense-nuxt-search-bar/ # Nuxt.js + Typesense search implementation
├── typesense-qwik-js-search/ # Qwik + Typesense search implementation
├── typesense-react-native-search-bar/ # React Native + Typesense search implementation
├── typesense-solid-js-search/ # SolidJS + Typesense search implementation
├── typesense-springboot-full-text-search/ # Spring Boot + Typesense backend implementation
├── typesense-node-prisma-full-text-search/ # Node.js (Express) + Typesense + Prisma backend implementation
├── typesense-node-sequelize-full-text-search/ # Node.js (Express) + Typesense + Sequelize backend implementation
├── typesense-vanilla-js-search/ # Vanilla JS + Typesense search implementation
└── README.md # You are here
```

## Projects
Expand All @@ -32,6 +35,9 @@ code-samples/
| [typesense-qwik-js-search](./typesense-qwik-js-search) | Qwik | Resumable search bar with real-time search and modern UI |
| [typesense-react-native-search-bar](./typesense-react-native-search-bar) | React Native | A mobile search bar with instant search capabilities |
| [typesense-solid-js-search](./typesense-solid-js-search) | SolidJS | A modern search bar with instant search capabilities |
| [typesense-springboot-full-text-search](./typesense-springboot-full-text-search) | Spring Boot | Backend API with full-text search using Typesense |
| [typesense-node-prisma-full-text-search](./typesense-node-prisma-full-text-search) | Node.js (Express) + Typesense + Prisma | Backend API with full-text search using Typesense |
| [typesense-node-sequelize-full-text-search](./typesense-node-sequelize-full-text-search) | Node.js (Express) + Typesense + Sequelize | Backend API with full-text search using Typesense |
| [typesense-vanilla-js-search](./typesense-vanilla-js-search) | Vanilla JS | A modern search bar with instant search capabilities |

## Getting Started
Expand Down
15 changes: 15 additions & 0 deletions typesense-node-prisma-full-text-search/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Server Configuration
PORT=3000

# Database Configuration
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=typesense_books
DB_PORT=5432

# Typesense Configuration
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=xyz
5 changes: 5 additions & 0 deletions typesense-node-prisma-full-text-search/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
# Keep environment variables out of version control
.env

/src/generated/prisma
231 changes: 231 additions & 0 deletions typesense-node-prisma-full-text-search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Node.js Express Full-Text Search with Typesense

A production-ready RESTful search API built with Node.js, Express, PostgreSQL (Prisma), and Typesense. Features full-text search, CRUD operations, real-time async indexing, and background sync workers.

## Tech Stack

- Node.js
- Express
- PostgreSQL with Prisma ORM
- Typesense
- TypeScript
- Docker

## Prerequisites

- Node.js v18+ installed
- Docker (for Typesense and PostgreSQL)
- Basic knowledge of REST APIs and SQL

## Quick Start

### 1. Clone the repository

```bash
git clone https://github.com/typesense/code-samples.git
cd typesense-node-prisma-full-text-search
```

### 2. Install dependencies

```bash
npm install
```

### 3. Start Typesense and PostgreSQL

Run Typesense and PostgreSQL using Docker:

```bash
# Start Typesense (replace TYPESENSE_VERSION with the latest from https://typesense.org/docs/guide/install-typesense.html)
docker run -d \
-p 8108:8108 \
-v typesense-data:/data \
typesense/typesense:27.1 \
--data-dir /data \
--api-key=xyz \
--enable-cors

# Start PostgreSQL
docker run -d \
-p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=typesense_books \
-v postgres-data:/var/lib/postgresql/data \
postgres:15
```

### 4. Set up environment variables

Create a `.env` file in the project root by copying `.env.example`:

```bash
cp .env.example .env
```

### 5. Project Structure

```text
├── prisma/
│ └── schema.prisma # Prisma schema and model definitions
├── src/
│ ├── config/
│ │ ├── database.ts # Prisma Client instantiation
│ │ └── env.ts # Environment variable validation
│ ├── routes/
│ │ ├── books.ts # CRUD endpoints for books
│ │ └── search.ts # Search and sync endpoints
│ ├── search/
│ │ ├── client.ts # Typesense client initialization
│ │ ├── collections.ts # Typesense collection schema
│ │ ├── sync.ts # Sync logic (incremental, full, soft delete)
│ │ └── worker.ts # Background sync worker
│ └── server.ts # Main application entry point
├── package.json
├── tsconfig.json
└── .env
```

### 6. Database Migrations

**Development Environment:**
When building out your schema or making changes during development, use the `db push` command. It pushes your schema state directly to the database without generating history:
```bash
npx prisma db push
```

**Production Environment:**
For production, you should use Prisma Migrate to generate and apply consistent database migrations.
Generate a migration (run this in dev when ready):
```bash
npx prisma migrate dev --name init_books
```
Apply migrations safely in production (e.g., during your CI/CD pipeline):
```bash
npx prisma migrate deploy
```

### 7. Start the development server

```bash
npm run dev
```

The server will automatically restart when you make changes to any TypeScript file.

Open [http://localhost:3000](http://localhost:3000) in your browser.

### 8. API Endpoints

#### Search

```bash
GET /search?q=<query>
```

Example:

```bash
curl "http://localhost:3000/search?q=harry"
```

#### CRUD Operations

**Create a book:**

```bash
POST /books
Content-Type: application/json

{
"title": "The Go Programming Language",
"authors": ["Alan Donovan", "Brian Kernighan"],
"publication_year": 2015,
"average_rating": 4.5,
"image_url": "https://example.com/image.jpg",
"ratings_count": 1000
}
```

**Get a book:**

```bash
GET /books/:id
```

**Get all books (with pagination):**

```bash
GET /books?page=1&limit=10
```

**Update a book:**

```bash
PUT /books/:id
Content-Type: application/json

{
"title": "Updated Title",
"authors": ["Author Name"],
"publication_year": 2024,
"average_rating": 4.8,
"image_url": "https://example.com/updated.jpg",
"ratings_count": 1500
}
```

**Delete a book (soft delete):**

```bash
DELETE /books/:id
```

#### Sync Operations

**Trigger manual sync:**

```bash
POST /sync
```

**Check sync status:**

```bash
GET /sync/status
```

### 9. How It Works

#### Architecture

```plaintext
User Request
Express API (CRUD)
PostgreSQL (Source of Truth)
Async Sync → Typesense (Search Index)
Background Worker (Every 60s)
```

#### Sync Strategies

##### 1. Startup Sync (Smart)

On every server start, the sync worker checks whether the Typesense collection already has documents. If empty, it seeds `lastSyncTime` to zero and runs a full sync. If it has data, it runs an incremental sync since `MAX(updated_at)` of PostgreSQL books table.

##### 2. Real-time Sync (Async)

Triggered on Create, Update, Delete operations in the background.

##### 3. Background Periodic Sync

Runs every 60 seconds automatically, doing incremental sync.

##### 4. Manual Sync

Endpoint: `POST /sync`
30 changes: 30 additions & 0 deletions typesense-node-prisma-full-text-search/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "typesense-node-prisma-search-app",
"version": "1.0.0",
"description": "A production-ready RESTful search API built with Node.js, Express, PostgreSQL, and Typesense.",
"main": "dist/server.js",
"scripts": {
"start": "node dist/server.js",
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"build": "tsc"
},
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"node-cron": "^3.0.3",
"pg": "^8.20.0",
"typesense": "^1.8.2"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.12.7",
"@types/node-cron": "^3.0.11",
"prisma": "^7.8.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
}
}
14 changes: 14 additions & 0 deletions typesense-node-prisma-full-text-search/prisma.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "books" (
"id" SERIAL NOT NULL,
"title" VARCHAR(255) NOT NULL,
"authors" JSONB NOT NULL DEFAULT '[]',
"publication_year" INTEGER,
"average_rating" DECIMAL(3,2),
"image_url" VARCHAR(255),
"ratings_count" INTEGER,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),

CONSTRAINT "books_pkey" PRIMARY KEY ("id")
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
22 changes: 22 additions & 0 deletions typesense-node-prisma-full-text-search/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
}

model Book {
id Int @id @default(autoincrement())
title String @db.VarChar(255)
authors Json @default("[]")
publication_year Int?
average_rating Decimal? @db.Decimal(3, 2)
image_url String? @db.VarChar(255)
ratings_count Int?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted_at DateTime?

@@map("books")
}
13 changes: 13 additions & 0 deletions typesense-node-prisma-full-text-search/src/config/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';

const connectionString = process.env.DATABASE_URL;

const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);

export const prisma = new PrismaClient({
adapter,
log: ['query', 'info', 'warn', 'error'],
});
Loading