diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..5175c803
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.php]
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..1f459efa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+* text=auto eol=lf
+
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+/tests export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+phpstan.neon export-ignore
+phpunit.xml export-ignore
+pint.json export-ignore
+testbench.yaml export-ignore
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
new file mode 100644
index 00000000..845bc8c0
--- /dev/null
+++ b/.github/workflows/testing.yml
@@ -0,0 +1,100 @@
+name: Testing
+
+on:
+  push:
+  schedule:
+    - cron: '0 0 * * 1' # run tests on every week Monday
+
+jobs:
+  static_analyze:
+    name: Static Analyze
+
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup PHP with PECL extension
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: 8.3
+          coverage: xdebug
+
+      - name: Get composer cache directory
+        run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+
+      - name: Cache dependencies
+        uses: actions/cache@v4
+        with:
+          path: ${{ env.COMPOSER_DIR }}
+          key: ${{ runner.os }}-composer-static-analyze-${{ hashFiles('**/composer.json') }}
+          restore-keys: ${{ runner.os }}-composer-static-analyze-
+
+      - name: Install dependencies
+        run: composer update --no-progress --no-interaction
+
+      - name: Check runtime dependencies
+        run: composer check-platform-reqs
+
+      - name: Run composer validate
+        run: composer validate --strict
+
+      - name: Run composer normalize
+        run: composer normalize --dry-run
+
+      - name: Run static analysis
+        run: vendor/bin/phpstan --memory-limit=-1 --verbose
+
+      - name: Run coding style checker
+        run: vendor/bin/pint -v --test
+
+      - name: Run type coverage check
+        run: vendor/bin/pest --memory-limit=-1 --type-coverage --min=95
+
+  testing:
+    name: PHP ${{ matrix.php }}
+
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+
+      matrix:
+        php: [ '8.3', '8.2' ]
+
+    services:
+      typesense:
+        image: typesense/typesense:0.25.2
+        ports:
+            - 8108:8108/tcp
+        volumes:
+            - typesense_data:/data
+        env:
+          TYPESENSE_DATA_DIR: /data
+          TYPESENSE_API_KEY: testing
+          TYPESENSE_ENABLE_CORS: true
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup PHP with PECL extension
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          coverage: xdebug
+
+      - name: Get composer cache directory
+        run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
+
+      - name: Cache dependencies
+        uses: actions/cache@v4
+        with:
+          path: ${{ env.COMPOSER_DIR }}
+          key: ${{ runner.os }}-composer-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}
+          restore-keys: ${{ runner.os }}-composer-php-${{ matrix.php }}-
+
+      - name: Install dependencies
+        run: composer update --no-progress --no-interaction
+
+      - name: Run tests
+        run: vendor/bin/pest --coverage --min=95
diff --git a/.gitignore b/.gitignore
index 8228c840..4d7bb9f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,11 @@
-.idea
-.tmp
-/composer.lock
-vendor
+/.fleet
+/.idea
+/.vscode
+/coverage
+/vendor
+.DS_Store
+.phpunit.result.cache
+clover.xml
+composer.phar
+composer.lock
+Thumbs.db
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 757297e7..3d4f9943 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,8 @@
 {
   "name": "typesense/typesense-php",
   "description": "PHP client for Typesense Search Server: https://github.com/typesense/typesense",
-  "type": "library",
-  "homepage": "https://github.com/typesense/typesense-php",
   "license": "Apache-2.0",
+  "type": "library",
   "authors": [
     {
       "name": "Typesense",
@@ -16,48 +15,61 @@
       "email": "abdullah@devloops.net",
       "homepage": "https://www.devloops.net",
       "role": "Developer"
+    },
+    {
+      "name": "bepsvpt",
+      "email": "6ibrl@cpp.tw"
     }
   ],
+  "homepage": "https://github.com/typesense/typesense-php",
   "support": {
-    "docs": "https://typesense.org/api",
+    "issues": "https://github.com/typesense/typesense-php/issues",
     "source": "https://github.com/typesense/typesense-php",
-    "issues": "https://github.com/typesense/typesense-php/issues"
+    "docs": "https://typesense.org/docs/api"
+  },
+  "require": {
+    "php": "^8.2",
+    "php-http/discovery": "^1.19",
+    "psr/http-client": "^1.0",
+    "psr/http-client-implementation": "^1.0",
+    "psr/http-factory": "^1.0",
+    "psr/http-factory-implementation": "^1.0"
+  },
+  "require-dev": {
+    "ergebnis/composer-normalize": "^2.42",
+    "guzzlehttp/guzzle": "^7.8",
+    "laravel/pint": "^1.14",
+    "pestphp/pest": "^2.34",
+    "pestphp/pest-plugin-faker": "^2.0",
+    "pestphp/pest-plugin-type-coverage": "^2.8",
+    "phpstan/phpstan": "^1.10",
+    "symfony/http-client": "^7.0"
   },
   "minimum-stability": "stable",
+  "prefer-stable": true,
   "autoload": {
     "psr-4": {
       "Typesense\\": "src/"
     }
   },
-  "require": {
-    "php": ">=7.4",
-    "ext-json": "*",
-    "monolog/monolog": "^2.1 || ^3.0 || ^3.3",
-    "nyholm/psr7": "^1.3",
-    "php-http/client-common": "^1.0 || ^2.3",
-    "php-http/discovery": "^1.0",
-    "php-http/httplug": "^1.0 || ^2.2",
-    "psr/http-client-implementation": "^1.0",
-    "psr/http-message": "^1.0 || ^2.0",
-    "psr/http-factory": "^1.0"
-  },
-  "require-dev": {
-    "squizlabs/php_codesniffer": "3.*",
-    "symfony/http-client": "^5.2"
+  "autoload-dev": {
+    "psr-4": {
+      "Typesense\\Tests\\": "tests/"
+    }
   },
   "config": {
-    "optimize-autoloader": true,
-    "preferred-install": {
-      "*": "dist"
+    "allow-plugins": {
+      "ergebnis/composer-normalize": true,
+      "pestphp/pest-plugin": true,
+      "php-http/discovery": false
     },
+    "optimize-autoloader": true,
     "sort-packages": true
   },
   "scripts": {
-    "typesenseServer": [
+    "typesense-server": [
       "Composer\\Config::disableProcessTimeout",
-      "docker-compose up"
-    ],
-    "lint": "phpcs -v",
-    "lint:fix": "phpcbf"
+      "docker-compose pull && docker-compose up"
+    ]
   }
 }
diff --git a/docker-compose.yml b/docker-compose.yml
index 41be3e38..9260c431 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,13 +1,16 @@
-version: '3.5'
+version: "3"
 
 services:
-    typesense:
-        image: typesense/typesense:0.21.0.rc20
-        environment:
-            TYPESENSE_DATA_DIR: /data
-            TYPESENSE_API_KEY: xyz
-        volumes:
-            - /tmp/typesense-server-data:/data
-        ports:
-            - 8108:8108
-        restart: "no"
+  typesense:
+    image: typesense/typesense:0.25.2
+    container_name: typesense-testing
+    restart: "on-failure"
+    ports:
+      - "8108:8108/tcp"
+    environment:
+      TYPESENSE_DATA_DIR: "/var/tmp"
+      TYPESENSE_API_KEY: "testing"
+      TYPESENSE_ENABLE_CORS: "true"
+      TYPESENSE_PEERING_ADDRESS: "127.0.0.1"
+      TYPESENSE_PEERING_PORT: "12345"
+      TYPESENSE_PEERING_SUBNET: "127.0.0.1/24"
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 91e87432..00000000
--- a/examples/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-### Running the examples
-
-Start a local typesense server via docker:
-
-```shell script
-composer run-script typesenseServer
-```
-
-Then:
-
-```shell script 
-cd examples
-php <example_name>.php
-```
\ No newline at end of file
diff --git a/examples/alias_operations.php b/examples/alias_operations.php
deleted file mode 100644
index 47f51e81..00000000
--- a/examples/alias_operations.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-
-/** @noinspection ForgottenDebugOutputInspection */
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-    try {
-        print_r($client->aliases['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    try {
-        print_r($client->collections['books_january']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books_january',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create Collection Alias-------\n";
-    print_r(
-        $client->aliases->upsert(
-            'books',
-            [
-                'collection_name' => 'books_january',
-            ]
-        )
-    );
-    echo "--------Create Collection Alias-------\n";
-    echo "\n";
-    echo "--------Create Document on Alias-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document on Alias-------\n";
-    echo "\n";
-    echo "--------Search Document on Alias-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hunger',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document on Alias-------\n";
-    echo "\n";
-    echo "--------Retrieve All Aliases-------\n";
-    print_r($client->aliases->retrieve());
-    echo "--------Retrieve All Aliases-------\n";
-    echo "\n";
-    echo "--------Retrieve All Alias Documents-------\n";
-    print_r($client->aliases['books']->retrieve());
-    echo "--------Retrieve All Alias Documents-------\n";
-    echo "\n";
-    echo "--------Delete Alias-------\n";
-    print_r($client->aliases['books']->delete());
-    echo "--------Delete Alias-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/cluster_operations.php b/examples/cluster_operations.php
deleted file mode 100644
index 98893507..00000000
--- a/examples/cluster_operations.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?php
-
-/** @noinspection ForgottenDebugOutputInspection */
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-
-    print_r($client->operations->perform('snapshot', ['snapshot_path' => '/tmp/snapshot']));
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/collection_operations.php b/examples/collection_operations.php
deleted file mode 100644
index 7f892c0e..00000000
--- a/examples/collection_operations.php
+++ /dev/null
@@ -1,232 +0,0 @@
-<?php
-
-/** @noinspection ForgottenDebugOutputInspection */
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Retrieve Collection-------\n";
-    print_r($client->collections['books']->retrieve());
-    echo "--------Retrieve Collection-------\n";
-    echo "\n";
-    echo "--------Retrieve All Collections-------\n";
-    print_r($client->collections->retrieve());
-    echo "--------Retrieve All Collections-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-
-    echo "--------Upsert Document-------\n";
-    print_r(
-        $client->collections['books']->documents->upsert(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.6,
-                'publication_year' => "2008",
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ],
-            [
-                'dirty_values' => 'coerce_or_reject',
-            ]
-        )
-    );
-    echo "--------Upsert Document-------\n";
-    echo "\n";
-
-    echo "--------Export Documents-------\n";
-    $exportedDocStrs = $client->collections['books']->documents->export(["exclude_fields" => "authors_facet"]);
-    print_r($exportedDocStrs);
-    echo "--------Export Documents-------\n";
-    echo "\n";
-    echo "--------Update Single Document-------\n";
-    print_r($client->collections['books']->documents['1']->update([
-        'average_rating' => 4.5,
-    ]));
-    echo "--------Update Single Document-------\n";
-    echo "\n";
-    echo "--------Fetch Single Document-------\n";
-    print_r($client->collections['books']->documents['1']->retrieve());
-    echo "--------Fetch Single Document-------\n";
-    echo "\n";
-    echo "--------Search Document-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hunger',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document-------\n";
-    echo "\n";
-    echo "--------Multi search-------\n";
-    print_r(
-        $client->multiSearch->perform(
-            [
-                'searches' => [
-                    [
-                        'q' => 'hunger',
-                        'sort_by' => 'ratings_count:desc',
-                    ],
-                    [
-                        'q' => 'game',
-                        'sort_by' => 'ratings_count:asc',
-                    ]
-                ]
-            ],
-            [
-                'query_by' => 'title',
-                'collection' => 'books'
-            ]
-        )
-    );
-    echo "--------Multi Search-------\n";
-    echo "\n";
-    echo "--------Delete Document-------\n";
-    print_r($client->collections['books']->documents['1']->delete());
-    echo "--------Delete Document-------\n";
-    echo "\n";
-    echo "--------Import Documents-------\n";
-    $docsToImport = [];
-    $exportedDocStrsArray = explode('\n', $exportedDocStrs);
-    foreach ($exportedDocStrsArray as $exportedDocStr) {
-        $docsToImport[] = json_decode($exportedDocStr, true);
-    }
-    $importRes =
-        $client->collections['books']->documents->import($docsToImport);
-    print_r($importRes);
-
-    // Or if you have documents in JSONL format, and want to save the overhead of parsing JSON,
-    // you can also pass in a JSONL string of documents
-    // $client->collections['books']->documents->import($exportedDocStrsArray);
-    echo "--------Import Documents-------\n";
-    echo "\n";
-    echo "--------Upsert Documents-------\n";
-    $upsertRes =
-        $client->collections['books']->documents->import($docsToImport, [
-            'action' => 'upsert'
-        ]);
-    print_r($upsertRes);
-    echo "--------Upsert Documents-------\n";
-    echo "\n";
-    echo "--------Update Documents-------\n";
-    $upsertRes =
-        $client->collections['books']->documents->import($docsToImport, [
-            'action' => 'update'
-        ]);
-    print_r($upsertRes);
-    echo "--------Upsert Documents-------\n";
-    echo "\n";
-    echo "--------Bulk Delete Documents-------\n";
-    print_r($client->collections['books']->documents->delete(['filter_by' => 'publication_year:=2008']));
-    echo "--------Bulk Delete Documents-------\n";
-    echo "\n";
-    echo "--------Delete Collection-------\n";
-    print_r($client->collections['books']->delete());
-    echo "--------Delete Collection-------\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/curation_operations.php b/examples/curation_operations.php
deleted file mode 100644
index 13c3874c..00000000
--- a/examples/curation_operations.php
+++ /dev/null
@@ -1,148 +0,0 @@
-<?php
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create or Update Override-------\n";
-    print_r(
-        $client->collections['books']->overrides->upsert(
-            'hermione-exact',
-            [
-                'rule' => [
-                    'query' => 'hermione',
-                    'match' => 'exact',
-                ],
-                'includes' => [
-                    [
-                        'id' => '1',
-                        'position' => 1,
-                    ],
-                ],
-            ]
-        )
-    );
-    echo "--------Create or Update Override-------\n";
-    echo "\n";
-    echo "--------Get All Overrides-------\n";
-    print_r($client->collections['books']->overrides->retrieve());
-    echo "--------Get All Overrides-------\n";
-    echo "\n";
-    echo "--------Get Single Override-------\n";
-    print_r(
-        $client->collections['books']->overrides['hermione-exact']->retrieve()
-    );
-    echo "--------Get Single Override-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-    echo "--------Search Document-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hermione',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document-------\n";
-    echo "\n";
-    echo "--------Delete Override-------\n";
-    print_r(
-        $client->collections['books']->getOverrides()['hermione-exact']->delete()
-    );
-    echo "--------Delete Override-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/info_operations.php b/examples/info_operations.php
deleted file mode 100644
index ba00a3e8..00000000
--- a/examples/info_operations.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-/** @noinspection ForgottenDebugOutputInspection */
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-
-    print_r($client->debug->retrieve());
-    print_r($client->metrics->retrieve());
-    print_r($client->health->retrieve());
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/keys_operations.php b/examples/keys_operations.php
deleted file mode 100644
index b4dcc9c8..00000000
--- a/examples/keys_operations.php
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-    try {
-        print_r($client->collections['users']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'users',
-                'fields' => [
-                    [
-                        'name' => 'company_id',
-                        'type' => 'int32',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'user_name',
-                        'type' => 'string',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'login_count',
-                        'type' => 'int32',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'country',
-                        'type' => 'string',
-                        'facet' => true
-                    ]
-                ],
-                'default_sorting_field' => 'company_id'
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create Documents-------\n";
-    print_r(
-        $client->collections['users']->documents->createMany([
-            [
-                'company_id' => 124,
-                'user_name' => 'Hilary Bradford',
-                'login_count' => 10,
-                'country' => 'USA'
-            ],
-            [
-                'company_id' => 124,
-                'user_name' => 'Nile Carty',
-                'login_count' => 100,
-                'country' => 'USA'
-            ],
-            [
-                'company_id' => 126,
-                'user_name' => 'Tahlia Maxwell',
-                'login_count' => 1,
-                'country' => 'France'
-            ],
-            [
-                'company_id' => 126,
-                'user_name' => 'Karl Roy',
-                'login_count' => 2,
-                'country' => 'Germany'
-            ]
-        ])
-    );
-    echo "--------Create Documents-------\n";
-    echo "\n";
-    echo "--------Create a search only API key-------\n";
-    $searchOnlyApiKeyResponse = $client->keys->create([
-        'description' => 'Search-only key.',
-        'actions' => ['documents:search'],
-        'collections' => ['*']
-    ]);
-    print_r($searchOnlyApiKeyResponse);
-    echo "--------Create a search only API key-------\n";
-    echo "\n";
-    echo "--------Get All Keys-------\n";
-    print_r($client->keys->retrieve());
-    echo "--------Get All Keys-------\n";
-    echo "\n";
-    echo "--------Get Single Key-------\n";
-    print_r(
-        $client->keys[$searchOnlyApiKeyResponse['id']]->retrieve()
-    );
-    echo "--------Get Single Key-------\n";
-    echo "\n";
-    echo "--------Generate Scoped API Key-------\n";
-    $scopedAPIKey = $client->keys->generateScopedSearchKey($searchOnlyApiKeyResponse['value'], ['filter_by' => 'company_id:124']);
-    print_r($scopedAPIKey);
-    echo "\n";
-    echo "--------Generate Scoped API Key-------\n";
-    echo "\n";
-    echo "--------Search Documents with scoped Key-------\n";
-    $scopedClient = new Client(
-        [
-            'api_key' => $scopedAPIKey,
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ]
-        ]
-    );
-
-    print_r(
-        $scopedClient->collections['users']->documents->search(
-            [
-                'q' => 'Hilary',
-                'query_by' => 'user_name'
-            ]
-        )
-    );
-    echo "--------Search Documents with scoped Key-------\n";
-    echo "\n";
-    echo "--------Search for document outside of scope for scoped Key-------\n";
-    print_r(
-        $scopedClient->collections['users']->documents->search(
-            [
-                'q' => 'Maxwell',
-                'query_by' => 'user_name'
-            ]
-        )
-    );
-    echo "--------Search for document outside of scope for scoped Key-------\n";
-    echo "\n";
-    echo "--------Delete Key-------\n";
-    print_r(
-        $client->keys[$searchOnlyApiKeyResponse['id']]->delete()
-    );
-    echo "--------Delete Key-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/synonym_operations.php b/examples/synonym_operations.php
deleted file mode 100644
index a883e8e5..00000000
--- a/examples/synonym_operations.php
+++ /dev/null
@@ -1,150 +0,0 @@
-<?php
-
-include '../vendor/autoload.php';
-
-use Symfony\Component\HttpClient\HttplugClient;
-use Typesense\Client;
-
-try {
-    $client = new Client(
-        [
-            'api_key' => 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '<pre>';
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                        'facet' => true
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Upsert Synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms->upsert(
-            'synonym-set-1',
-            [
-                'synonyms' => ['Hunger', 'Katniss'],
-            ]
-        )
-    );
-    echo "--------Upsert Synonym-------\n";
-    echo "\n";
-    echo "--------Get All Synonyms-------\n";
-    print_r($client->collections['books']->synonyms->retrieve());
-    echo "--------Get All Synonyms-------\n";
-    echo "\n";
-    echo "--------Get Single Synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms['synonym-set-1']->retrieve()
-    );
-    echo "--------Get Single Synonym-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-    echo "--------Search Document, using a synonym-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'Katniss',
-                'query_by' => 'title'
-            ]
-        )
-    );
-    echo "--------Search Document, using a synonym-------\n";
-    echo "\n";
-    echo "--------Upsert 1-way synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms->upsert(
-            'synonym-set-1',
-            [
-                'root' => 'Katniss',
-                'synonyms' => ['Hunger', 'Peeta'],
-            ]
-        )
-    );
-    echo "--------Upsert 1-way synonym-------\n";
-    echo "\n";
-    echo "--------Search Document, using a synonym-------\n";
-    // Won't return any results
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'Peeta',
-                'query_by' => 'title'
-            ]
-        )
-    );
-    echo "--------Search Document, using a synonym-------\n";
-    echo "\n";
-    echo "--------Delete Synonym-------\n";
-    print_r(
-        $client->collections['books']->getSynonyms()['synonym-set-1']->delete()
-    );
-    echo "--------Delete Synonym-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index e7ff5dab..00000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0"?>
-<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PHP_CodeSniffer"
-         xsi:noNamespaceSchemaLocation="phpcs.xsd">
-    <description>The coding standard for PHP_CodeSniffer itself.</description>
-
-    <file>src</file>
-    <file>examples</file>
-
-    <exclude-pattern>*/src/Standards/*/Tests/*\.(inc|css|js)$</exclude-pattern>
-    <exclude-pattern>*/tests/Core/*/*\.(inc|css|js)$</exclude-pattern>
-
-    <arg name="basepath" value="."/>
-    <arg name="colors"/>
-    <arg name="parallel" value="75"/>
-    <arg value="np"/>
-
-    <!-- Don't hide tokenizer exceptions -->
-    <rule ref="Internal.Tokenizer.Exception">
-        <type>error</type>
-    </rule>
-
-    <!-- Include the whole PSR12 standard -->
-    <rule ref="PSR12"/>
-
-    <!-- Have 12 chars padding maximum and always show as errors -->
-    <rule ref="Generic.Formatting.MultipleStatementAlignment">
-        <properties>
-            <property name="maxPadding" value="12"/>
-            <property name="error" value="true"/>
-        </properties>
-    </rule>
-
-    <!-- Ban some functions -->
-    <rule ref="Generic.PHP.ForbiddenFunctions">
-        <properties>
-            <property name="forbiddenFunctions" type="array">
-                <element key="sizeof" value="count"/>
-                <element key="delete" value="unset"/>
-                <element key="print" value="echo"/>
-                <element key="is_null" value="null"/>
-                <element key="create_function" value="null"/>
-            </property>
-        </properties>
-    </rule>
-</ruleset>
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 00000000..fb46dc56
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,19 @@
+parameters:
+  level: max
+
+  parallel:
+    maximumNumberOfProcesses: 4
+
+  paths:
+    - src
+    - tests
+
+  ignoreErrors:
+    -
+      message: '#Undefined variable\: \$this#'
+      paths:
+        - tests/*
+    -
+      message: '#Call to an undefined method Pest\\PendingCalls\\TestCall\|Pest\\Support\\HigherOrderTapProxy\:\:.+\(\)\.#'
+      paths:
+        - tests/*
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 00000000..b8cda39a
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
+  bootstrap="vendor/autoload.php"
+  beStrictAboutChangesToGlobalState="true"
+  colors="true"
+  columns="max"
+  executionOrder="random"
+  displayDetailsOnTestsThatTriggerWarnings="true"
+  failOnDeprecation="true"
+  failOnWarning="true"
+  failOnNotice="true"
+  failOnRisky="true"
+>
+  <source>
+    <include>
+      <directory>src</directory>
+    </include>
+  </source>
+  <testsuites>
+    <testsuite name="Test Suite">
+      <directory>tests</directory>
+    </testsuite>
+  </testsuites>
+  <coverage>
+    <report>
+      <html outputDirectory="coverage" lowUpperBound="80" highLowerBound="95"/>
+    </report>
+  </coverage>
+  <php>
+    <ini name="memory_limit" value="-1"/>
+    <ini name="error_reporting" value="-1"/>
+    <ini name="log_errors_max_len" value="0"/>
+    <ini name="xdebug.show_exception_trace" value="0"/>
+    <ini name="assert.exception" value="1"/>
+  </php>
+</phpunit>
diff --git a/src/Alias.php b/src/Alias.php
deleted file mode 100644
index 5dd76189..00000000
--- a/src/Alias.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Alias
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Alias
-{
-
-    /**
-     * @var string
-     */
-    private string $name;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param string $name
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $name, ApiCall $apiCall)
-    {
-        $this->name    = $name;
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    public function endPointPath(): string
-    {
-        return sprintf('%s/%s', Aliases::RESOURCE_PATH, $this->name);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Aliases.php b/src/Aliases.php
deleted file mode 100644
index d1c76cf1..00000000
--- a/src/Aliases.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Aliases
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Aliases implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/aliases';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $aliases = [];
-
-    /**
-     * Aliases constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $aliasName
-     *
-     * @return string
-     */
-    public function endPointPath(string $aliasName): string
-    {
-        return sprintf('%s/%s', static::RESOURCE_PATH, $aliasName);
-    }
-
-    /**
-     * @param string $name
-     * @param array $mapping
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $name, array $mapping): array
-    {
-        return $this->apiCall->put($this->endPointPath($name), $mapping);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @param $name
-     *
-     * @return mixed
-     */
-    public function __get($name)
-    {
-        if (isset($this->{$name})) {
-            return $this->{$name};
-        }
-
-        if (!isset($this->aliases[$name])) {
-            $this->aliases[$name] = new Alias($name, $this->apiCall);
-        }
-
-        return $this->aliases[$name];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->aliases[$offset]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($offset): Alias
-    {
-        if (!isset($this->aliases[$offset])) {
-            $this->aliases[$offset] = new Alias($offset, $this->apiCall);
-        }
-
-        return $this->aliases[$offset];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->aliases[$offset] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($offset): void
-    {
-        unset($this->aliases[$offset]);
-    }
-}
diff --git a/src/Analytics.php b/src/Analytics.php
deleted file mode 100644
index 0adf4fd2..00000000
--- a/src/Analytics.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace Typesense;
-
-class Analytics
-{
-    const RESOURCE_PATH = '/analytics';
-
-    private ApiCall $apiCall;
-
-    private AnalyticsRules $rules;
-
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    public function rules()
-    {
-        if (!isset($this->rules)) {
-            $this->rules = new AnalyticsRules($this->apiCall);
-        }
-        return $this->rules;
-    }
-}
diff --git a/src/AnalyticsRule.php b/src/AnalyticsRule.php
deleted file mode 100644
index bc8755f6..00000000
--- a/src/AnalyticsRule.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-
-namespace Typesense;
-
-class AnalyticsRule
-{
-    private $ruleName;
-    private ApiCall $apiCall;
-
-    public function __construct(string $ruleName, ApiCall $apiCall)
-    {
-        $this->ruleName = $ruleName;
-        $this->apiCall = $apiCall;
-    }
-
-    public function retrieve()
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    public function delete()
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-
-    private function endpointPath()
-    {
-        return AnalyticsRules::RESOURCE_PATH . '/' . $this->ruleName;
-    }
-}
diff --git a/src/AnalyticsRules.php b/src/AnalyticsRules.php
deleted file mode 100644
index 412aea0d..00000000
--- a/src/AnalyticsRules.php
+++ /dev/null
@@ -1,39 +0,0 @@
-<?php
-
-namespace Typesense;
-
-class AnalyticsRules
-{
-    const RESOURCE_PATH = '/analytics/rules';
-
-    private ApiCall $apiCall;
-    private $analyticsRules = [];
-
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    public function __get($ruleName)
-    {
-        if (!isset($this->analyticsRules[$ruleName])) {
-            $this->analyticsRules[$ruleName] = new AnalyticsRule($ruleName, $this->apiCall);
-        }
-        return $this->analyticsRules[$ruleName];
-    }
-
-    public function upsert($ruleName, $params)
-    {
-        return $this->apiCall->put($this->endpoint_path($ruleName), $params);
-    }
-
-    public function retrieve()
-    {
-        return $this->apiCall->get($this->endpoint_path(), []);
-    }
-
-    private function endpoint_path($operation = null)
-    {
-        return self::RESOURCE_PATH . ($operation === null ? '' : "/$operation");
-    }
-}
diff --git a/src/ApiCall.php b/src/ApiCall.php
deleted file mode 100644
index 435ff107..00000000
--- a/src/ApiCall.php
+++ /dev/null
@@ -1,369 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Exception;
-use Http\Client\Exception as HttpClientException;
-use Http\Client\Exception\HttpException;
-use Psr\Http\Client\ClientInterface;
-use Psr\Http\Message\StreamInterface;
-use Psr\Log\LoggerInterface;
-use Typesense\Exceptions\HTTPStatus0Error;
-use Typesense\Exceptions\ObjectAlreadyExists;
-use Typesense\Exceptions\ObjectNotFound;
-use Typesense\Exceptions\ObjectUnprocessable;
-use Typesense\Exceptions\RequestMalformed;
-use Typesense\Exceptions\RequestUnauthorized;
-use Typesense\Exceptions\ServerError;
-use Typesense\Exceptions\ServiceUnavailable;
-use Typesense\Exceptions\TypesenseClientError;
-use Typesense\Lib\Configuration;
-use Typesense\Lib\Node;
-
-/**
- * Class ApiCall
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ApiCall
-{
-
-    private const API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY';
-
-    /**
-     * @var ClientInterface
-     */
-    private ClientInterface $client;
-
-    /**
-     * @var Configuration
-     */
-    private Configuration $config;
-
-    /**
-     * @var array|Node[]
-     */
-    private static array $nodes;
-
-    /**
-     * @var Node|null
-     */
-    private static ?Node $nearestNode;
-
-    /**
-     * @var int
-     */
-    private int $nodeIndex;
-
-    /**
-     * @var LoggerInterface
-     */
-    public LoggerInterface $logger;
-
-    /**
-     * ApiCall constructor.
-     *
-     * @param Configuration $config
-     */
-    public function __construct(Configuration $config)
-    {
-        $this->config        = $config;
-        $this->logger        = $config->getLogger();
-        $this->client        = $config->getClient();
-        static::$nodes       = $this->config->getNodes();
-        static::$nearestNode = $this->config->getNearestNode();
-        $this->nodeIndex     = 0;
-        $this->initializeNodes();
-    }
-
-    /**
-     *  Initialize Nodes
-     */
-    private function initializeNodes(): void
-    {
-        if (static::$nearestNode !== null) {
-            $this->setNodeHealthCheck(static::$nearestNode, true);
-        }
-
-        foreach (static::$nodes as &$node) {
-            $this->setNodeHealthCheck($node, true);
-        }
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $params
-     * @param bool $asJson
-     *
-     * @return string|array
-     * @throws TypesenseClientError
-     * @throws Exception|HttpClientException
-     */
-    public function get(string $endPoint, array $params, bool $asJson = true)
-    {
-        return $this->makeRequest('get', $endPoint, $asJson, [
-            'query' => $params ?? [],
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param mixed $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array|string
-     * @throws TypesenseClientError
-     * @throws HttpClientException
-     */
-    public function post(string $endPoint, $body, bool $asJson = true, array $queryParameters = [])
-    {
-        return $this->makeRequest('post', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function put(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('put', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function patch(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('patch', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(string $endPoint, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('delete', $endPoint, $asJson, [
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * Makes the actual http request, along with retries
-     *
-     * @param string $method
-     * @param string $endPoint
-     * @param bool $asJson
-     * @param array $options
-     *
-     * @return string|array
-     * @throws TypesenseClientError|HttpClientException
-     * @throws Exception
-     */
-    private function makeRequest(string $method, string $endPoint, bool $asJson, array $options)
-    {
-        $numRetries    = 0;
-        $lastException = null;
-        while ($numRetries < $this->config->getNumRetries() + 1) {
-            $numRetries++;
-            $node = $this->getNode();
-
-            try {
-                $url   = $node->url() . $endPoint;
-                $reqOp = $this->getRequestOptions();
-                if (isset($options['data'])) {
-                    if (is_string($options['data']) || $options['data'] instanceof StreamInterface) {
-                        $reqOp['body'] = $options['data'];
-                    } else {
-                        $reqOp['body'] = \json_encode($options['data']);
-                    }
-                }
-
-                if (isset($options['query'])) {
-                    foreach ($options['query'] as $key => $value) :
-                        if (is_bool($value)) {
-                            $options['query'][$key] = ($value) ? 'true' : 'false';
-                        }
-                    endforeach;
-                    $reqOp['query'] = http_build_query($options['query']);
-                }
-
-                $response = $this->client->send(
-                    \strtoupper($method),
-                    $url . '?' . ($reqOp['query'] ?? ''),
-                    $reqOp['headers'] ?? [],
-                    $reqOp['body'] ?? null
-                );
-
-                $statusCode = $response->getStatusCode();
-                if (0 < $statusCode && $statusCode < 500) {
-                    $this->setNodeHealthCheck($node, true);
-                }
-
-                if (!(200 <= $statusCode && $statusCode < 300)) {
-                    $errorMessage = json_decode($response->getBody()
-                            ->getContents(), true, 512, JSON_THROW_ON_ERROR)['message'] ?? 'API error.';
-                    throw $this->getException($statusCode)
-                        ->setMessage($errorMessage);
-                }
-
-                return $asJson ? json_decode($response->getBody()
-                    ->getContents(), true, 512, JSON_THROW_ON_ERROR) : $response->getBody()
-                    ->getContents();
-            } catch (HttpException $exception) {
-                if (
-                    $exception->getResponse()
-                        ->getStatusCode() === 408
-                ) {
-                    continue;
-                }
-                $this->setNodeHealthCheck($node, false);
-                throw $this->getException($exception->getResponse()
-                    ->getStatusCode())
-                    ->setMessage($exception->getMessage());
-            } catch (TypesenseClientError | HttpClientException $exception) {
-                $this->setNodeHealthCheck($node, false);
-                throw $exception;
-            } catch (Exception $exception) {
-                $this->setNodeHealthCheck($node, false);
-                $lastException = $exception;
-                sleep($this->config->getRetryIntervalSeconds());
-            }
-        }
-
-        if ($lastException) {
-            throw $lastException;
-        }
-    }
-
-    /**
-     * @return array
-     */
-    private function getRequestOptions(): array
-    {
-        return [
-            'headers' => [
-                static::API_KEY_HEADER_NAME => $this->config->getApiKey(),
-            ]
-        ];
-    }
-
-    /**
-     * @param Node $node
-     *
-     * @return bool
-     */
-    private function nodeDueForHealthCheck(Node $node): bool
-    {
-        $currentTimestamp = time();
-        return ($currentTimestamp - $node->getLastAccessTs()) > $this->config->getHealthCheckIntervalSeconds();
-    }
-
-    /**
-     * @param Node $node
-     * @param bool $isHealthy
-     */
-    public function setNodeHealthCheck(Node $node, bool $isHealthy): void
-    {
-        $node->setHealthy($isHealthy);
-        $node->setLastAccessTs(time());
-    }
-
-    /**
-     * Returns a healthy host from the pool in a round-robin fashion
-     * Might return an unhealthy host periodically to check for recovery.
-     *
-     * @return Node
-     */
-    public function getNode(): Lib\Node
-    {
-        if (static::$nearestNode !== null) {
-            if (static::$nearestNode->isHealthy() || $this->nodeDueForHealthCheck(static::$nearestNode)) {
-                return static::$nearestNode;
-            }
-        }
-        $i = 0;
-        while ($i < count(static::$nodes)) {
-            $i++;
-            $node            = static::$nodes[$this->nodeIndex];
-            $this->nodeIndex = ($this->nodeIndex + 1) % count(static::$nodes);
-            if ($node->isHealthy() || $this->nodeDueForHealthCheck($node)) {
-                return $node;
-            }
-        }
-
-        /**
-         * None of the nodes are marked healthy, but some of them could have become healthy since last health check.
-         * So we will just return the next node.
-         */
-        return static::$nodes[$this->nodeIndex];
-    }
-
-    /**
-     * @param int $httpCode
-     *
-     * @return TypesenseClientError
-     */
-    public function getException(int $httpCode): TypesenseClientError
-    {
-        switch ($httpCode) {
-            case 0:
-                return new HTTPStatus0Error();
-            case 400:
-                return new RequestMalformed();
-            case 401:
-                return new RequestUnauthorized();
-            case 404:
-                return new ObjectNotFound();
-            case 409:
-                return new ObjectAlreadyExists();
-            case 422:
-                return new ObjectUnprocessable();
-            case 500:
-                return new ServerError();
-            case 503:
-                return new ServiceUnavailable();
-            default:
-                return new TypesenseClientError();
-        }
-    }
-
-    /**
-     * @return LoggerInterface
-     */
-    public function getLogger()
-    {
-        return $this->logger;
-    }
-}
diff --git a/src/Client.php b/src/Client.php
deleted file mode 100644
index d64be61f..00000000
--- a/src/Client.php
+++ /dev/null
@@ -1,180 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Typesense\Exceptions\ConfigError;
-use Typesense\Lib\Configuration;
-
-/**
- * Class Client
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Client
-{
-    /**
-     * @var Configuration
-     */
-    private Configuration $config;
-
-    /**
-     * @var Collections
-     */
-    public Collections $collections;
-
-    /**
-     * @var Aliases
-     */
-    public Aliases $aliases;
-
-    /**
-     * @var Keys
-     */
-    public Keys $keys;
-
-    /**
-     * @var Debug
-     */
-    public Debug $debug;
-
-    /**
-     * @var Metrics
-     */
-    public Metrics $metrics;
-
-    /**
-     * @var Health
-     */
-    public Health $health;
-
-    /**
-     * @var Operations
-     */
-    public Operations $operations;
-
-    /**
-     * @var MultiSearch
-     */
-    public MultiSearch $multiSearch;
-
-    /**
-     * @var Presets
-     */
-    public Presets $presets;
-
-    /**
-     * @var Analytics
-     */
-    public Analytics $analytics;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Client constructor.
-     *
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    public function __construct(array $config)
-    {
-        $this->config  = new Configuration($config);
-        $this->apiCall = new ApiCall($this->config);
-
-        $this->collections = new Collections($this->apiCall);
-        $this->aliases     = new Aliases($this->apiCall);
-        $this->keys        = new Keys($this->apiCall);
-        $this->debug       = new Debug($this->apiCall);
-        $this->metrics     = new Metrics($this->apiCall);
-        $this->health      = new Health($this->apiCall);
-        $this->operations  = new Operations($this->apiCall);
-        $this->multiSearch = new MultiSearch($this->apiCall);
-        $this->presets     = new Presets($this->apiCall);
-        $this->analytics   = new Analytics($this->apiCall);
-    }
-
-    /**
-     * @return Collections
-     */
-    public function getCollections(): Collections
-    {
-        return $this->collections;
-    }
-
-    /**
-     * @return Aliases
-     */
-    public function getAliases(): Aliases
-    {
-        return $this->aliases;
-    }
-
-    /**
-     * @return Keys
-     */
-    public function getKeys(): Keys
-    {
-        return $this->keys;
-    }
-
-    /**
-     * @return Debug
-     */
-    public function getDebug(): Debug
-    {
-        return $this->debug;
-    }
-
-    /**
-     * @return Metrics
-     */
-    public function getMetrics(): Metrics
-    {
-        return $this->metrics;
-    }
-
-    /**
-     * @return Health
-     */
-    public function getHealth(): Health
-    {
-        return $this->health;
-    }
-
-    /**
-     * @return Operations
-     */
-    public function getOperations(): Operations
-    {
-        return $this->operations;
-    }
-
-    /**
-     * @return MultiSearch
-     */
-    public function getMultiSearch(): MultiSearch
-    {
-        return $this->multiSearch;
-    }
-
-    /**
-     * @return Presets
-     */
-    public function getPresets(): Presets
-    {
-        return $this->presets;
-    }
-
-    /**
-     * @return Analytics
-     */
-    public function getAnalytics(): Analytics
-    {
-        return $this->analytics;
-    }
-}
diff --git a/src/Collection.php b/src/Collection.php
deleted file mode 100644
index 91748e32..00000000
--- a/src/Collection.php
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Collection
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Collection
-{
-
-    /**
-     * @var string
-     */
-    private string $name;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var Documents
-     */
-    public Documents $documents;
-
-    /**
-     * @var Overrides
-     */
-    public Overrides $overrides;
-
-    /**
-     * @var Synonyms
-     */
-    public Synonyms $synonyms;
-
-    /**
-     * Collection constructor.
-     *
-     * @param string $name
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $name, ApiCall $apiCall)
-    {
-        $this->name      = $name;
-        $this->apiCall   = $apiCall;
-        $this->documents = new Documents($name, $this->apiCall);
-        $this->overrides = new Overrides($name, $this->apiCall);
-        $this->synonyms  = new Synonyms($name, $this->apiCall);
-    }
-
-    /**
-     * @return string
-     */
-    public function endPointPath(): string
-    {
-        return sprintf('%s/%s', Collections::RESOURCE_PATH, $this->name);
-    }
-
-    /**
-     * @return Documents
-     */
-    public function getDocuments(): Documents
-    {
-        return $this->documents;
-    }
-
-    /**
-     * @return Overrides
-     */
-    public function getOverrides(): Overrides
-    {
-        return $this->overrides;
-    }
-
-    /**
-     * @return Synonyms
-     */
-    public function getSynonyms(): Synonyms
-    {
-        return $this->synonyms;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $schema): array
-    {
-        return $this->apiCall->patch($this->endPointPath(), $schema);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Collections.php b/src/Collections.php
deleted file mode 100644
index 9385aa38..00000000
--- a/src/Collections.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Collections
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Collections implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/collections';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $collections = [];
-
-    /**
-     * Collections constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param $collectionName
-     *
-     * @return mixed
-     */
-    public function __get($collectionName)
-    {
-        if (isset($this->{$collectionName})) {
-            return $this->{$collectionName};
-        }
-        if (!isset($this->collections[$collectionName])) {
-            $this->collections[$collectionName] = new Collection($collectionName, $this->apiCall);
-        }
-
-        return $this->collections[$collectionName];
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $schema): array
-    {
-        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->collections[$offset]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($offset): Collection
-    {
-        if (!isset($this->collections[$offset])) {
-            $this->collections[$offset] = new Collection($offset, $this->apiCall);
-        }
-
-        return $this->collections[$offset];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->collections[$offset] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($offset): void
-    {
-        unset($this->collections[$offset]);
-    }
-}
diff --git a/src/Debug.php b/src/Debug.php
deleted file mode 100644
index c6987387..00000000
--- a/src/Debug.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Debug
- *
- * @package \Typesense
- * @date    10/12/20
- */
-class Debug
-{
-    public const RESOURCE_PATH = '/debug';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Debug::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/Document.php b/src/Document.php
deleted file mode 100644
index 617bf9ee..00000000
--- a/src/Document.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Document
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Document
-{
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var string
-     */
-    private string $documentId;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Document constructor.
-     *
-     * @param string $collectionName
-     * @param string $documentId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, string $documentId, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->documentId     = $documentId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endpointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            Documents::RESOURCE_PATH,
-            $this->documentId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    /**
-     * @param array $partialDocument
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $partialDocument, array $options = []): array
-    {
-        return $this->apiCall->patch($this->endpointPath(), $partialDocument, true, $options);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-}
diff --git a/src/Documents.php b/src/Documents.php
deleted file mode 100644
index 39c3bc6e..00000000
--- a/src/Documents.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Documents
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Documents implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = 'documents';
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $documents = [];
-
-    /**
-     * Documents constructor.
-     *
-     * @param string $collectionName
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $action
-     *
-     * @return string
-     */
-    private function endPointPath(string $action = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $action
-        );
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $document, array $options = []): array
-    {
-        return $this->apiCall->post($this->endPointPath(''), $document, true, $options);
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(array $document, array $options = []): array
-    {
-        return $this->apiCall->post(
-            $this->endPointPath(''),
-            $document,
-            true,
-            array_merge($options, ['action' => 'upsert'])
-        );
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $document, array $options = []): array
-    {
-        return $this->apiCall->post(
-            $this->endPointPath(''),
-            $document,
-            true,
-            array_merge($options, ['action' => 'update'])
-        );
-    }
-
-    /**
-     * @param array $documents
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException|\JsonException
-     */
-    public function createMany(array $documents, array $options = []): array
-    {
-        $this->apiCall->getLogger()->warning(
-            "createMany is deprecated and will be removed in a future version. " .
-            "Use import instead, which now takes both an array of documents or a JSONL string of documents"
-        );
-        return $this->import($documents, $options);
-    }
-
-    /**
-     * @param string|array $documents
-     * @param array $options
-     *
-     * @return string|array
-     * @throws TypesenseClientError
-     * @throws \JsonException|HttpClientException
-     */
-    public function import($documents, array $options = [])
-    {
-        if (is_array($documents)) {
-            $documentsInJSONLFormat = implode(
-                "\n",
-                array_map(
-                    static fn(array $document) => json_encode($document, JSON_THROW_ON_ERROR),
-                    $documents
-                )
-            );
-        } else {
-            $documentsInJSONLFormat = $documents;
-        }
-        $resultsInJSONLFormat = $this->apiCall->post(
-            $this->endPointPath('import'),
-            $documentsInJSONLFormat,
-            false,
-            $options
-        );
-
-        if (is_array($documents)) {
-            return array_map(static function ($item) {
-                return json_decode($item, true, 512, JSON_THROW_ON_ERROR);
-            }, explode("\n", $resultsInJSONLFormat));
-        } else {
-            return $resultsInJSONLFormat;
-        }
-    }
-
-    /**
-     * @param array $queryParams
-     *
-     * @return string
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function export(array $queryParams = []): string
-    {
-        return $this->apiCall->get($this->endPointPath('export'), $queryParams, false);
-    }
-
-    /**
-     * @param array $queryParams
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(array $queryParams = []): array
-    {
-        return $this->apiCall->delete($this->endPointPath(), true, $queryParams);
-    }
-
-    /**
-     * @param array $searchParams
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function search(array $searchParams): array
-    {
-        return $this->apiCall->get($this->endPointPath('search'), $searchParams);
-    }
-
-    /**
-     * @param mixed $documentId
-     *
-     * @return bool
-     */
-    public function offsetExists($documentId): bool
-    {
-        return isset($this->documents[$documentId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($documentId): Document
-    {
-        if (!isset($this->documents[$documentId])) {
-            $this->documents[$documentId] = new Document($this->collectionName, $documentId, $this->apiCall);
-        }
-
-        return $this->documents[$documentId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($documentId): void
-    {
-        if (isset($this->documents[$documentId])) {
-            unset($this->documents[$documentId]);
-        }
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->documents[$offset] = $value;
-    }
-}
diff --git a/src/Exceptions/Client/ClientErrorException.php b/src/Exceptions/Client/ClientErrorException.php
new file mode 100644
index 00000000..37a9e92e
--- /dev/null
+++ b/src/Exceptions/Client/ClientErrorException.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+use Typesense\Exceptions\TypesenseException;
+
+abstract class ClientErrorException extends TypesenseException
+{
+    //
+}
diff --git a/src/Exceptions/Client/InvalidPayloadException.php b/src/Exceptions/Client/InvalidPayloadException.php
new file mode 100644
index 00000000..beb55b72
--- /dev/null
+++ b/src/Exceptions/Client/InvalidPayloadException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+class InvalidPayloadException extends ClientErrorException
+{
+    //
+}
diff --git a/src/Exceptions/Client/ResourceAlreadyExistsException.php b/src/Exceptions/Client/ResourceAlreadyExistsException.php
new file mode 100644
index 00000000..0117838c
--- /dev/null
+++ b/src/Exceptions/Client/ResourceAlreadyExistsException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+class ResourceAlreadyExistsException extends ClientErrorException
+{
+    //
+}
diff --git a/src/Exceptions/Client/ResourceNotFoundException.php b/src/Exceptions/Client/ResourceNotFoundException.php
new file mode 100644
index 00000000..fe4770fd
--- /dev/null
+++ b/src/Exceptions/Client/ResourceNotFoundException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+class ResourceNotFoundException extends ClientErrorException
+{
+    //
+}
diff --git a/src/Exceptions/Client/UnauthorizedException.php b/src/Exceptions/Client/UnauthorizedException.php
new file mode 100644
index 00000000..78d6e4f4
--- /dev/null
+++ b/src/Exceptions/Client/UnauthorizedException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+class UnauthorizedException extends ClientErrorException
+{
+    //
+}
diff --git a/src/Exceptions/Client/UnprocessableEntityException.php b/src/Exceptions/Client/UnprocessableEntityException.php
new file mode 100644
index 00000000..17c86a59
--- /dev/null
+++ b/src/Exceptions/Client/UnprocessableEntityException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Client;
+
+class UnprocessableEntityException extends ClientErrorException
+{
+    //
+}
diff --git a/src/Exceptions/ConfigError.php b/src/Exceptions/ConfigError.php
deleted file mode 100644
index b4fcc7cb..00000000
--- a/src/Exceptions/ConfigError.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ConfigError
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ConfigError extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/HTTPStatus0Error.php b/src/Exceptions/HTTPStatus0Error.php
deleted file mode 100644
index e2f2752c..00000000
--- a/src/Exceptions/HTTPStatus0Error.php
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class HTTPStatus0Error
- *
- * @date 9/3/20
- * @author Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class HTTPStatus0Error extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/MalformedResponsePayloadException.php b/src/Exceptions/MalformedResponsePayloadException.php
new file mode 100644
index 00000000..4465b787
--- /dev/null
+++ b/src/Exceptions/MalformedResponsePayloadException.php
@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions;
+
+class MalformedResponsePayloadException extends TypesenseException
+{
+    /**
+     * Constructor.
+     */
+    public function __construct(
+        public string $context,
+    ) {
+        parent::__construct();
+    }
+}
diff --git a/src/Exceptions/ObjectAlreadyExists.php b/src/Exceptions/ObjectAlreadyExists.php
deleted file mode 100644
index 956cffd1..00000000
--- a/src/Exceptions/ObjectAlreadyExists.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ObjectAlreadyExists
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ObjectAlreadyExists extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ObjectNotFound.php b/src/Exceptions/ObjectNotFound.php
deleted file mode 100644
index 7cae4b82..00000000
--- a/src/Exceptions/ObjectNotFound.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ObjectNotFound
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ObjectNotFound extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ObjectUnprocessable.php b/src/Exceptions/ObjectUnprocessable.php
deleted file mode 100644
index 89a2f922..00000000
--- a/src/Exceptions/ObjectUnprocessable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ObjectUnprocessable
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ObjectUnprocessable extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/RequestMalformed.php b/src/Exceptions/RequestMalformed.php
deleted file mode 100644
index 2a7c42b7..00000000
--- a/src/Exceptions/RequestMalformed.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class RequestMalformed
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class RequestMalformed extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/RequestUnauthorized.php b/src/Exceptions/RequestUnauthorized.php
deleted file mode 100644
index 47ec3533..00000000
--- a/src/Exceptions/RequestUnauthorized.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class RequestUnauthorized
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class RequestUnauthorized extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/Server/ServerErrorException.php b/src/Exceptions/Server/ServerErrorException.php
new file mode 100644
index 00000000..bc3b0ccd
--- /dev/null
+++ b/src/Exceptions/Server/ServerErrorException.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Server;
+
+use Typesense\Exceptions\TypesenseException;
+
+abstract class ServerErrorException extends TypesenseException
+{
+    //
+}
diff --git a/src/Exceptions/Server/ServiceUnavailableException.php b/src/Exceptions/Server/ServiceUnavailableException.php
new file mode 100644
index 00000000..99bf778c
--- /dev/null
+++ b/src/Exceptions/Server/ServiceUnavailableException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions\Server;
+
+class ServiceUnavailableException extends ServerErrorException
+{
+    //
+}
diff --git a/src/Exceptions/ServerError.php b/src/Exceptions/ServerError.php
deleted file mode 100644
index e13d18d0..00000000
--- a/src/Exceptions/ServerError.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ServerError
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ServerError extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ServiceUnavailable.php b/src/Exceptions/ServiceUnavailable.php
deleted file mode 100644
index 1b32bcb1..00000000
--- a/src/Exceptions/ServiceUnavailable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class ServiceUnavailable
- *
- * @package \Typesense\Exceptions
- * @date    4/8/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class ServiceUnavailable extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/Timeout.php b/src/Exceptions/Timeout.php
deleted file mode 100644
index 359eb338..00000000
--- a/src/Exceptions/Timeout.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-/**
- * Class Timeout
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Timeout extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/TypesenseClientError.php b/src/Exceptions/TypesenseClientError.php
deleted file mode 100644
index 2f20c2a8..00000000
--- a/src/Exceptions/TypesenseClientError.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-namespace Typesense\Exceptions;
-
-use Exception;
-
-/**
- * Class TypesenseClientError
- *
- * @package \Typesense\Exceptions
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class TypesenseClientError extends Exception
-{
-
-    public function setMessage(string $message): TypesenseClientError
-    {
-        $this->message = $message;
-        return $this;
-    }
-}
diff --git a/src/Exceptions/TypesenseException.php b/src/Exceptions/TypesenseException.php
new file mode 100644
index 00000000..13959fcd
--- /dev/null
+++ b/src/Exceptions/TypesenseException.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions;
+
+use Exception;
+
+abstract class TypesenseException extends Exception
+{
+    //
+}
diff --git a/src/Exceptions/UnknownHttpException.php b/src/Exceptions/UnknownHttpException.php
new file mode 100644
index 00000000..ac2b9877
--- /dev/null
+++ b/src/Exceptions/UnknownHttpException.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Exceptions;
+
+class UnknownHttpException extends TypesenseException
+{
+    //
+}
diff --git a/src/Health.php b/src/Health.php
deleted file mode 100644
index f0db09ed..00000000
--- a/src/Health.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Health
- *
- * @package \Typesense
- * @date    10/12/20
- */
-class Health
-{
-    public const RESOURCE_PATH = '/health';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Health::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/Http.php b/src/Http.php
new file mode 100644
index 00000000..a90530c0
--- /dev/null
+++ b/src/Http.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense;
+
+use Http\Discovery\Psr18Client;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * @phpstan-import-type TypesenseConfiguration from Typesense
+ */
+class Http
+{
+    public Psr18Client $client;
+
+    /**
+     * @param  TypesenseConfiguration  $config
+     */
+    public function __construct(
+        public array $config,
+    ) {
+        $this->client = new Psr18Client($config['http'] ?? null);
+    }
+
+    /**
+     * @param  'GET'|'HEAD'|'POST'|'PATCH'|'PUT'|'DELETE'  $method
+     */
+    public function request(string $method, string $path, string $body = ''): ResponseInterface
+    {
+        $request = $this->client
+            ->createRequest($method, $this->uri($path))
+            ->withHeader('Content-Type', 'application/json')
+            ->withHeader('X-TYPESENSE-API-KEY', $this->config['apiKey']);
+
+        if (! empty($body)) {
+            $request = $request->withBody(
+                $this->client->createStream($body),
+            );
+        }
+
+        return $this->client->sendRequest($request);
+    }
+
+    /**
+     * Form a complete request URL.
+     */
+    public function uri(string $path): string
+    {
+        return sprintf(
+            '%s/%s',
+            rtrim($this->config['url'], '/'),
+            ltrim($path, '/'),
+        );
+    }
+}
diff --git a/src/Key.php b/src/Key.php
deleted file mode 100644
index af7a9b1f..00000000
--- a/src/Key.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Key
- *
- * @package \Typesense
- * @date 6/1/20
- * @author Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Key
-{
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var string
-     */
-    private string $keyId;
-
-    /**
-     * Key constructor.
-     *
-     * @param string $keyId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $keyId, ApiCall $apiCall)
-    {
-        $this->keyId   = $keyId;
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endpointPath(): string
-    {
-        return sprintf('%s/%s', Keys::RESOURCE_PATH, $this->keyId);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-}
diff --git a/src/Keys.php b/src/Keys.php
deleted file mode 100644
index 740d9098..00000000
--- a/src/Keys.php
+++ /dev/null
@@ -1,129 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Keys
- *
- * @package \Typesense
- * @date 6/1/20
- * @author Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Keys implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/keys';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $keys = [];
-
-    /**
-     * Keys constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $schema): array
-    {
-        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
-    }
-
-    /**
-     * @param string $searchKey
-     * @param array $parameters
-     *
-     * @return string
-     * @throws \JsonException
-     */
-    public function generateScopedSearchKey(
-        string $searchKey,
-        array $parameters
-    ): string {
-        $paramStr = json_encode($parameters, JSON_THROW_ON_ERROR);
-        $digest = base64_encode(
-            hash_hmac(
-                'sha256',
-                mb_convert_encoding($paramStr, 'UTF-8', 'ISO-8859-1'),
-                mb_convert_encoding($searchKey, 'UTF-8', 'ISO-8859-1'),
-                true)
-        );
-        $keyPrefix = substr($searchKey, 0, 4);
-        $rawScopedKey = sprintf(
-            '%s%s%s',
-            mb_convert_encoding($digest, 'ISO-8859-1', 'UTF-8'),
-            $keyPrefix,
-            $paramStr
-        );
-        return base64_encode(mb_convert_encoding($rawScopedKey, 'UTF-8', 'ISO-8859-1'));
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @param mixed $offset
-     *
-     * @return bool
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->keys[$offset]);
-    }
-
-    /**
-     * @param mixed $offset
-     *
-     * @return \Typesense\Key
-     */
-    public function offsetGet($offset): Key
-    {
-        if (!isset($this->keys[$offset])) {
-            $this->keys[$offset] = new Key($offset, $this->apiCall);
-        }
-
-        return $this->keys[$offset];
-    }
-
-    /**
-     * @param mixed $offset
-     * @param mixed $value
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->keys[$offset] = $value;
-    }
-
-    /**
-     * @param mixed $offset
-     */
-    public function offsetUnset($offset): void
-    {
-        unset($this->keys[$offset]);
-    }
-}
diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php
deleted file mode 100644
index 54f0e9ea..00000000
--- a/src/Lib/Configuration.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-
-namespace Typesense\Lib;
-
-use Http\Client\Common\HttpMethodsClient;
-use Http\Discovery\Psr17FactoryDiscovery;
-use Http\Discovery\Psr18ClientDiscovery;
-use Monolog\Handler\StreamHandler;
-use Monolog\Logger;
-use Psr\Http\Client\ClientInterface;
-use Psr\Log\LoggerInterface;
-use Typesense\Exceptions\ConfigError;
-
-/**
- * Class Configuration
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Configuration
-{
-
-    /**
-     * @var Node[]
-     */
-    private array $nodes;
-
-    /**
-     * @var Node|null
-     */
-    private ?Node $nearestNode;
-
-    /**
-     * @var string
-     */
-    private string $apiKey;
-
-    /**
-     * @var float
-     */
-    private float $numRetries;
-
-    /**
-     * @var float
-     */
-    private float $retryIntervalSeconds;
-
-    /**
-     * @var int
-     */
-    private int $healthCheckIntervalSeconds;
-
-    /**
-     * @var LoggerInterface
-     */
-    private LoggerInterface $logger;
-
-    /**
-     * @var null|ClientInterface
-     */
-    private ?ClientInterface $client = null;
-
-    /**
-     * @var int
-     */
-    private int $logLevel;
-
-    /**
-     * Configuration constructor.
-     *
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    public function __construct(array $config)
-    {
-        $this->validateConfigArray($config);
-
-        $nodes = $config['nodes'] ?? [];
-
-        foreach ($nodes as $node) {
-            $this->nodes[] = new Node($node['host'], $node['port'], $node['path'] ?? '', $node['protocol']);
-        }
-
-        $nearestNode       = $config['nearest_node'] ?? null;
-        $this->nearestNode = null;
-        if (null !== $nearestNode) {
-            $this->nearestNode =
-                new Node(
-                    $nearestNode['host'],
-                    $nearestNode['port'],
-                    $nearestNode['path'] ?? '',
-                    $nearestNode['protocol']
-                );
-        }
-
-        $this->apiKey = $config['api_key'] ?? '';
-        $this->healthCheckIntervalSeconds = (int)($config['healthcheck_interval_seconds'] ?? 60);
-        $this->numRetries           = (float)($config['num_retries'] ?? 3);
-        $this->retryIntervalSeconds = (float)($config['retry_interval_seconds'] ?? 1.0);
-
-        $this->logLevel = $config['log_level'] ?? Logger::WARNING;
-        $this->logger   = new Logger('typesense');
-        $this->logger->pushHandler(new StreamHandler('php://stdout', $this->logLevel));
-
-        if (true === \array_key_exists('client', $config) && $config['client'] instanceof ClientInterface) {
-            $this->client = $config['client'];
-        }
-    }
-
-    /**
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    private function validateConfigArray(array $config): void
-    {
-        $nodes = $config['nodes'] ?? false;
-        if (!$nodes) {
-            throw new ConfigError('`nodes` is not defined.');
-        }
-
-        $apiKey = $config['api_key'] ?? false;
-        if (!$apiKey) {
-            throw new ConfigError('`api_key` is not defined.');
-        }
-
-        foreach ($nodes as $node) {
-            if (!$this->validateNodeFields($node)) {
-                throw new ConfigError(
-                    '`node` entry be a dictionary with the following required keys: host, port, protocol, api_key'
-                );
-            }
-        }
-        $nearestNode = $config['nearest_node'] ?? [];
-        if (!empty($nearestNode) && !$this->validateNodeFields($nearestNode)) {
-            throw new ConfigError(
-                '`nearest_node` entry be a dictionary with the following required keys: host, port, protocol, api_key'
-            );
-        }
-    }
-
-    /**
-     * @param array $node
-     *
-     * @return bool
-     */
-    public function validateNodeFields(array $node): bool
-    {
-        $keys = [
-            'host',
-            'port',
-            'protocol',
-        ];
-        return !array_diff_key(array_flip($keys), $node);
-    }
-
-    /**
-     * @return Node[]
-     */
-    public function getNodes(): array
-    {
-        return $this->nodes;
-    }
-
-    /**
-     * @return Node
-     */
-    public function getNearestNode(): ?Node
-    {
-        return $this->nearestNode;
-    }
-
-    /**
-     * @return mixed|string
-     */
-    public function getApiKey()
-    {
-        return $this->apiKey;
-    }
-
-    /**
-     * @return float
-     */
-    public function getNumRetries(): float
-    {
-        return $this->numRetries;
-    }
-
-    /**
-     * @return float
-     */
-    public function getRetryIntervalSeconds(): float
-    {
-        return $this->retryIntervalSeconds;
-    }
-
-    /**
-     * @return float|mixed
-     */
-    public function getHealthCheckIntervalSeconds()
-    {
-        return $this->healthCheckIntervalSeconds;
-    }
-
-    /**
-     * @return LoggerInterface
-     */
-    public function getLogger(): LoggerInterface
-    {
-        return $this->logger;
-    }
-
-    /**
-     * @return ClientInterface
-     */
-    public function getClient(): ClientInterface
-    {
-        return new HttpMethodsClient(
-            $this->client ?? Psr18ClientDiscovery::find(),
-                Psr17FactoryDiscovery::findRequestFactory(),
-                Psr17FactoryDiscovery::findStreamFactory(),
-        );
-    }
-}
diff --git a/src/Lib/Node.php b/src/Lib/Node.php
deleted file mode 100644
index 3500baae..00000000
--- a/src/Lib/Node.php
+++ /dev/null
@@ -1,105 +0,0 @@
-<?php
-
-namespace Typesense\Lib;
-
-/**
- * Class Node
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Node
-{
-
-    /**
-     * @var string
-     */
-    private string $host;
-
-    /**
-     * @var string
-     */
-    private string $port;
-
-    /**
-     * @var string
-     */
-    private string $path;
-
-    /**
-     * @var string
-     */
-    private string $protocol;
-
-    /**
-     * @var bool
-     */
-    private bool $healthy = false;
-
-    /**
-     * @var int
-     */
-    private int $lastAccessTs;
-
-    /**
-     * Node constructor.
-     *
-     * @param string $host
-     * @param string $port
-     * @param string $path
-     * @param string $protocol
-     */
-    public function __construct(
-        string $host,
-        string $port,
-        string $path,
-        string $protocol
-    ) {
-        $this->host         = $host;
-        $this->port         = $port;
-        $this->path         = $path;
-        $this->protocol     = $protocol;
-        $this->lastAccessTs = time();
-    }
-
-    /**
-     * @return string
-     */
-    public function url(): string
-    {
-        return sprintf('%s://%s:%s%s', $this->protocol, $this->host, $this->port, $this->path);
-    }
-
-    /**
-     * @return bool
-     */
-    public function isHealthy(): bool
-    {
-        return $this->healthy;
-    }
-
-    /**
-     * @param bool $healthy
-     */
-    public function setHealthy(bool $healthy): void
-    {
-        $this->healthy = $healthy;
-    }
-
-    /**
-     * @return int
-     */
-    public function getLastAccessTs(): int
-    {
-        return $this->lastAccessTs;
-    }
-
-    /**
-     * @param int $lastAccessTs
-     */
-    public function setLastAccessTs(int $lastAccessTs): void
-    {
-        $this->lastAccessTs = $lastAccessTs;
-    }
-}
diff --git a/src/Metrics.php b/src/Metrics.php
deleted file mode 100644
index fb3c8b56..00000000
--- a/src/Metrics.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Metrics
- *
- * @package \Typesense
- * @date    10/12/20
- */
-class Metrics
-{
-    public const RESOURCE_PATH = '/metrics.json';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Metrics::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/MultiSearch.php b/src/MultiSearch.php
deleted file mode 100644
index d24f9e9d..00000000
--- a/src/MultiSearch.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class MultiSearch
- *
- * @package \Typesense
- */
-class MultiSearch
-{
-    public const RESOURCE_PATH = '/multi_search';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $searches
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function perform(array $searches, array $queryParameters = []): array
-    {
-        return $this->apiCall->post(
-            sprintf('%s', static::RESOURCE_PATH),
-            $searches,
-            true,
-            $queryParameters
-        );
-    }
-}
diff --git a/src/Objects/Alias.php b/src/Objects/Alias.php
new file mode 100644
index 00000000..191485e2
--- /dev/null
+++ b/src/Objects/Alias.php
@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+class Alias extends TypesenseObject
+{
+    public string $name;
+
+    public string $collection_name;
+}
diff --git a/src/Objects/Analytic.php b/src/Objects/Analytic.php
new file mode 100644
index 00000000..71e7606f
--- /dev/null
+++ b/src/Objects/Analytic.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+/**
+ * @phpstan-type RulePayload array{
+ *     source: array{
+ *         collections: array<int, string>,
+ *     },
+ *     destination: array{
+ *         collection: string,
+ *     },
+ *     limit: int,
+ * }
+ */
+class Analytic extends TypesenseObject
+{
+    public string $name;
+
+    public string $type;
+
+    public stdClass $params;
+}
diff --git a/src/Objects/Collection.php b/src/Objects/Collection.php
new file mode 100644
index 00000000..16ed65e4
--- /dev/null
+++ b/src/Objects/Collection.php
@@ -0,0 +1,54 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+class Collection extends TypesenseObject
+{
+    /**
+     * @var non-negative-int
+     */
+    public int $created_at;
+
+    public string $default_sorting_field;
+
+    public bool $enable_nested_fields;
+
+    /**
+     * @var array<int, CollectionField>
+     */
+    public array $fields;
+
+    public string $name;
+
+    /**
+     * @var non-negative-int
+     */
+    public int $num_documents;
+
+    /**
+     * @var array<int, string>
+     */
+    public array $symbols_to_index;
+
+    /**
+     * @var array<int, string>
+     */
+    public array $token_separators;
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function from(stdClass $data): static
+    {
+        $data->fields = array_map(
+            fn (stdClass $data) => CollectionField::from($data),
+            $data->fields,
+        );
+
+        return parent::from($data);
+    }
+}
diff --git a/src/Objects/CollectionDroppedField.php b/src/Objects/CollectionDroppedField.php
new file mode 100644
index 00000000..077a938a
--- /dev/null
+++ b/src/Objects/CollectionDroppedField.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+class CollectionDroppedField extends TypesenseObject
+{
+    public string $name;
+
+    public true $drop;
+
+    public mixed $embed;
+}
diff --git a/src/Objects/CollectionField.php b/src/Objects/CollectionField.php
new file mode 100644
index 00000000..ee797279
--- /dev/null
+++ b/src/Objects/CollectionField.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+/**
+ * @phpstan-type TypesenseCollectionFieldType 'string'|'string[]'|'int32'|'int32[]'|'int64'|'int64[]'|'float'|'float[]'|'bool'|'bool[]'|'geopoint'|'geopoint[]'|'object'|'object[]'|'string*'|'auto'
+ * @phpstan-type TypesenseCollectionFieldLocale 'ja'|'zh'|'ko'|'th'|'el'|'ru'|'sr'|'uk'|'be'
+ * @phpstan-type TypesenseCollectionField array{
+ *     embed: mixed,
+ *     facet: bool,
+ *     index: bool,
+ *     infix: bool,
+ *     locale: TypesenseCollectionFieldLocale,
+ *     name: string,
+ *     nested: bool,
+ *     nested_array: int,
+ *     num_dim: int,
+ *     optional: bool,
+ *     reference: bool,
+ *     sort: bool,
+ *     type: TypesenseCollectionFieldType,
+ *     vec_dist: TypesenseCollectionFieldType,
+ * }
+ */
+class CollectionField extends TypesenseObject
+{
+    public mixed $embed;
+
+    public bool $facet;
+
+    public bool $index;
+
+    public bool $infix;
+
+    /**
+     * @var TypesenseCollectionFieldLocale|''
+     */
+    public string $locale;
+
+    public string $name;
+
+    public bool $nested;
+
+    public int $nested_array;
+
+    public int $num_dim;
+
+    public bool $optional;
+
+    public string $reference;
+
+    public bool $sort;
+
+    /**
+     * @var TypesenseCollectionFieldType
+     */
+    public string $type;
+
+    public string $vec_dist;
+}
diff --git a/src/Objects/Curation.php b/src/Objects/Curation.php
new file mode 100644
index 00000000..b9d970e6
--- /dev/null
+++ b/src/Objects/Curation.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+/**
+ * @phpstan-type CurationRule array{
+ *     query?: string,
+ *     match?: 'exact'|'contains',
+ *     filter_by?: string,
+ * }
+ * @phpstan-type CurationExclude array{
+ *     id: string,
+ * }
+ * @phpstan-type CurationInclude array{
+ *     id: string,
+ *     position: int,
+ * }
+ */
+class Curation extends TypesenseObject
+{
+    public string $id;
+
+    /**
+     * @var stdClass{
+     *     query?: string,
+     *     match?: 'exact'|'contains',
+     *     filter_by?: string,
+     * }
+     */
+    public stdClass $rule;
+
+    /**
+     * @var array<int, CurationExclude>
+     */
+    public array $excludes = [];
+
+    /**
+     * @var array<int, CurationInclude>
+     */
+    public array $includes = [];
+
+    public ?string $filter_by = null;
+
+    public ?string $sort_by = null;
+
+    public ?string $replace_query = null;
+
+    public bool $remove_matched_tokens = true;
+
+    public bool $filter_curated_hits = false;
+
+    public ?int $effective_from_ts = null;
+
+    public ?int $effective_to_ts = null;
+
+    public bool $stop_processing = true;
+}
diff --git a/src/Objects/Document.php b/src/Objects/Document.php
new file mode 100644
index 00000000..270a141d
--- /dev/null
+++ b/src/Objects/Document.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+abstract class Document extends TypesenseObject
+{
+    public string $id;
+}
diff --git a/src/Objects/GenericDocument.php b/src/Objects/GenericDocument.php
new file mode 100644
index 00000000..e7d7f5e9
--- /dev/null
+++ b/src/Objects/GenericDocument.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use AllowDynamicProperties;
+
+#[AllowDynamicProperties]
+class GenericDocument extends Document
+{
+    //
+}
diff --git a/src/Objects/ImportedDocument.php b/src/Objects/ImportedDocument.php
new file mode 100644
index 00000000..c85d22a4
--- /dev/null
+++ b/src/Objects/ImportedDocument.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+class ImportedDocument extends TypesenseObject
+{
+    public bool $success;
+
+    public ?string $id = null;
+
+    public ?string $error = null;
+
+    public ?int $code = null;
+
+    public string|stdClass|null $document = null;
+}
diff --git a/src/Objects/Key.php b/src/Objects/Key.php
new file mode 100644
index 00000000..ac489509
--- /dev/null
+++ b/src/Objects/Key.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+/**
+ * @phpstan-type KeyAction 'collections:create'|'collections:delete'|'collections:get'|'collections:list'|'collections:*'|'documents:search'|'documents:get'|'documents:create'|'documents:upsert'|'documents:update'|'documents:delete'|'documents:import'|'documents:export'|'documents:*'|'aliases:list'|'aliases:get'|'aliases:create'|'aliases:delete'|'aliases:*'|'synonyms:list'|'synonyms:get'|'synonyms:create'|'synonyms:delete'|'synonyms:*'|'overrides:list'|'overrides:get'|'overrides:create'|'overrides:delete'|'overrides:*'|'keys:list'|'keys:get'|'keys:create'|'keys:delete'|'keys:*'|'metrics.json:list'|'stats.json:list'|'debug:list'|'*'
+ */
+class Key extends TypesenseObject
+{
+    /**
+     * @var non-empty-array<int, KeyAction>
+     */
+    public array $actions;
+
+    /**
+     * @var non-empty-array<int, string>
+     */
+    public array $collections;
+
+    public string $description;
+
+    public int $expires_at;
+
+    public int $id;
+
+    public ?string $value = null;
+
+    public ?string $value_prefix = null;
+}
diff --git a/src/Objects/Metric.php b/src/Objects/Metric.php
new file mode 100644
index 00000000..eb0514a8
--- /dev/null
+++ b/src/Objects/Metric.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+class Metric extends TypesenseObject
+{
+    public string $system_cpu1_active_percentage;
+
+    public ?string $system_cpu2_active_percentage = null;
+
+    public ?string $system_cpu3_active_percentage = null;
+
+    public ?string $system_cpu4_active_percentage = null;
+
+    public ?string $system_cpu5_active_percentage = null;
+
+    public ?string $system_cpu6_active_percentage = null;
+
+    public ?string $system_cpu7_active_percentage = null;
+
+    public ?string $system_cpu8_active_percentage = null;
+
+    public ?string $system_cpu9_active_percentage = null;
+
+    public ?string $system_cpu10_active_percentage = null;
+
+    public ?string $system_cpu11_active_percentage = null;
+
+    public ?string $system_cpu12_active_percentage = null;
+
+    public ?string $system_cpu13_active_percentage = null;
+
+    public ?string $system_cpu14_active_percentage = null;
+
+    public ?string $system_cpu15_active_percentage = null;
+
+    public ?string $system_cpu16_active_percentage = null;
+
+    public ?string $system_cpu17_active_percentage = null;
+
+    public ?string $system_cpu18_active_percentage = null;
+
+    public ?string $system_cpu19_active_percentage = null;
+
+    public ?string $system_cpu20_active_percentage = null;
+
+    public ?string $system_cpu21_active_percentage = null;
+
+    public ?string $system_cpu22_active_percentage = null;
+
+    public ?string $system_cpu23_active_percentage = null;
+
+    public ?string $system_cpu24_active_percentage = null;
+
+    public string $system_cpu_active_percentage;
+
+    public string $system_disk_total_bytes;
+
+    public string $system_disk_used_bytes;
+
+    public string $system_memory_total_bytes;
+
+    public string $system_memory_used_bytes;
+
+    public string $system_network_received_bytes;
+
+    public string $system_network_sent_bytes;
+
+    public string $typesense_memory_active_bytes;
+
+    public string $typesense_memory_allocated_bytes;
+
+    public string $typesense_memory_fragmentation_ratio;
+
+    public string $typesense_memory_mapped_bytes;
+
+    public string $typesense_memory_metadata_bytes;
+
+    public string $typesense_memory_resident_bytes;
+
+    public string $typesense_memory_retained_bytes;
+}
diff --git a/src/Objects/Stat.php b/src/Objects/Stat.php
new file mode 100644
index 00000000..99fd2a0b
--- /dev/null
+++ b/src/Objects/Stat.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+class Stat extends TypesenseObject
+{
+    public float $delete_latency_ms;
+
+    public float $delete_requests_per_second;
+
+    public float $import_latency_ms;
+
+    public float $import_requests_per_second;
+
+    public stdClass $latency_ms;
+
+    public float $overloaded_requests_per_second;
+
+    public float $pending_write_batches;
+
+    public stdClass $requests_per_second;
+
+    public float $search_latency_ms;
+
+    public float $search_requests_per_second;
+
+    public float $total_requests_per_second;
+
+    public float $write_latency_ms;
+
+    public float $write_requests_per_second;
+}
diff --git a/src/Objects/Synonym.php b/src/Objects/Synonym.php
new file mode 100644
index 00000000..95305c20
--- /dev/null
+++ b/src/Objects/Synonym.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+class Synonym extends TypesenseObject
+{
+    public string $id;
+
+    /**
+     * @var array<int, string>
+     */
+    public array $synonyms;
+
+    public string $root = '';
+
+    public string $locale = '';
+
+    /**
+     * @var array<int, string>
+     */
+    public array $symbols_to_index = [];
+}
diff --git a/src/Objects/TypesenseObject.php b/src/Objects/TypesenseObject.php
new file mode 100644
index 00000000..f6e8b0c7
--- /dev/null
+++ b/src/Objects/TypesenseObject.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Objects;
+
+use stdClass;
+
+abstract class TypesenseObject
+{
+    /**
+     * Constructor.
+     */
+    final public function __construct(
+        protected readonly stdClass $raw,
+    ) {
+        foreach (get_object_vars($this->raw) as $key => $value) {
+            $this->{$key} = $value;
+        }
+    }
+
+    /**
+     * Create object instance from raw data.
+     */
+    public static function from(stdClass $data): static
+    {
+        return new static($data);
+    }
+}
diff --git a/src/Operations.php b/src/Operations.php
deleted file mode 100644
index 661c5974..00000000
--- a/src/Operations.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Operations
- *
- * @package \Typesense
- */
-class Operations
-{
-    public const RESOURCE_PATH = '/operations';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $operationName
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function perform(string $operationName, array $queryParameters = []): array
-    {
-        return $this->apiCall->post(
-            sprintf('%s/%s', static::RESOURCE_PATH, $operationName),
-            null,
-            true,
-            $queryParameters
-        );
-    }
-}
diff --git a/src/Override.php b/src/Override.php
deleted file mode 100644
index d50855fe..00000000
--- a/src/Override.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Override
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Override
-{
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var string
-     */
-    private string $overrideId;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Override constructor.
-     *
-     * @param string $collectionName
-     * @param string $overrideId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, string $overrideId, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->overrideId     = $overrideId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endPointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            Overrides::RESOURCE_PATH,
-            $this->overrideId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Overrides.php b/src/Overrides.php
deleted file mode 100644
index a7cc48fa..00000000
--- a/src/Overrides.php
+++ /dev/null
@@ -1,119 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Overrides
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Overrides implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = 'overrides';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var array
-     */
-    private array $overrides = [];
-
-    /**
-     * Overrides constructor.
-     *
-     * @param string $collectionName
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $overrideId
-     *
-     * @return string
-     */
-    public function endPointPath(string $overrideId = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $overrideId
-        );
-    }
-
-    /**
-     * @param string $overrideId
-     * @param array $config
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $overrideId, array $config): array
-    {
-        return $this->apiCall->put($this->endPointPath($overrideId), $config);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($overrideId): bool
-    {
-        return isset($this->overrides[$overrideId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($overrideId): Override
-    {
-        if (!isset($this->overrides[$overrideId])) {
-            $this->overrides[$overrideId] = new Override($this->collectionName, $overrideId, $this->apiCall);
-        }
-
-        return $this->overrides[$overrideId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($overrideId, $value): void
-    {
-        $this->overrides[$overrideId] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($overrideId): void
-    {
-        unset($this->overrides[$overrideId]);
-    }
-}
diff --git a/src/Presets.php b/src/Presets.php
deleted file mode 100644
index b0f56c01..00000000
--- a/src/Presets.php
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Document
- *
- * @package \Typesense
- * @date    4/5/20
- * @author  Abdullah Al-Faqeir <abdullah@devloops.net>
- */
-class Presets
-{
-    /**
-     * @var ApiCall
-     */
-    private $apiCall;
-
-    public const PRESETS_PATH = '/presets';
-
-    public const MULTI_SEARCH_PATH = '/multi_search';
-
-    /**
-     * Document constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param $presetName
-     * @return array|string
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function searchWithPreset($presetName)
-    {
-        return $this->apiCall->post($this->multiSearchEndpointPath(), [], true, ['preset' => $presetName]);
-    }
-
-    /**
-     * @return array|string
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function get()
-    {
-        return $this->apiCall->get(static::PRESETS_PATH, []);
-    }
-
-    /**
-     * @param array $options
-     *
-     * @return array
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function put(array $options = [])
-    {
-        $presetName = $options['preset_name'];
-        $presetsData = $options['preset_data'];
-
-        return $this->apiCall->put($this->endpointPath($presetName), $presetsData);
-    }
-
-    /**
-     * @param $presetName
-     * @return array
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function delete($presetName)
-    {
-        return $this->apiCall->delete($this->endpointPath($presetName));
-    }
-
-    /**
-     * @param $presetsName
-     * @return string
-     */
-    private function endpointPath($presetsName)
-    {
-        return sprintf(
-            '%s/%s',
-            static::PRESETS_PATH,
-            $presetsName
-        );
-    }
-
-    /**
-     * @param $presetsName
-     * @return string
-     */
-    private function multiSearchEndpointPath()
-    {
-        return sprintf(
-            '%s',
-            static::MULTI_SEARCH_PATH
-        );
-    }
-}
diff --git a/src/Requests/Alias.php b/src/Requests/Alias.php
new file mode 100644
index 00000000..acc126b6
--- /dev/null
+++ b/src/Requests/Alias.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Alias as AliasObject;
+
+class Alias extends Request
+{
+    /**
+     * @param array{
+     *     collection_name: string,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#create-or-update-an-alias
+     */
+    public function upsert(string $name, array $payload): AliasObject
+    {
+        $path = sprintf('/aliases/%s', $name);
+
+        $data = $this->send('PUT', $path, $payload);
+
+        return AliasObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#retrieve-an-alias
+     */
+    public function retrieve(string $name): AliasObject
+    {
+        $path = sprintf('/aliases/%s', $name);
+
+        $data = $this->send('GET', $path);
+
+        return AliasObject::from($data);
+    }
+
+    /**
+     * @return array<int, AliasObject>
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#list-all-aliases
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/aliases');
+
+        return array_map(
+            fn (stdClass $datum) => AliasObject::from($datum),
+            $data->aliases,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#delete-an-alias
+     */
+    public function delete(string $name): bool
+    {
+        $path = sprintf('/aliases/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->name === $name;
+    }
+}
diff --git a/src/Requests/Analytic.php b/src/Requests/Analytic.php
new file mode 100644
index 00000000..96d99977
--- /dev/null
+++ b/src/Requests/Analytic.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceAlreadyExistsException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Analytic as AnalyticObject;
+
+/**
+ * @phpstan-import-type RulePayload from AnalyticObject
+ */
+class Analytic extends Request
+{
+    /**
+     * Create a collection for aggregation.
+     *
+     * @throws ResourceAlreadyExistsException
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#create-a-collection-for-aggregation
+     */
+    public function setup(string $name): bool
+    {
+        $data = $this->send('POST', '/collections', [
+            'name' => $name,
+            'fields' => [
+                ['name' => 'q', 'type' => 'string'],
+                ['name' => 'count', 'type' => 'int32'],
+            ],
+        ]);
+
+        return $data->name === $name;
+    }
+
+    /**
+     * @param array{
+     *     name: string,
+     *     type: 'popular_queries',
+     *     params: RulePayload,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#create-an-analytics-rule
+     */
+    public function create(array $payload): AnalyticObject
+    {
+        $data = $this->send('POST', '/analytics/rules', $payload);
+
+        return AnalyticObject::from($data);
+    }
+
+    /**
+     * @return array<int, AnalyticObject>
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#list-all-rules
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/analytics/rules');
+
+        return array_map(
+            fn (stdClass $datum) => AnalyticObject::from($datum),
+            $data->rules,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#remove-a-rule
+     */
+    public function delete(string $name): bool
+    {
+        $path = sprintf('/analytics/rules/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->name === $name;
+    }
+}
diff --git a/src/Requests/Cluster.php b/src/Requests/Cluster.php
new file mode 100644
index 00000000..2f513462
--- /dev/null
+++ b/src/Requests/Cluster.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use Typesense\Objects\Metric;
+use Typesense\Objects\Stat;
+
+class Cluster extends Request
+{
+    /**
+     * Creates a point-in-time snapshot of a Typesense node's state and data in the specified directory.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#create-snapshot-for-backups
+     */
+    public function snapshot(string $path): bool
+    {
+        $path = sprintf('/operations/snapshot?snapshot_path=%s', $path);
+
+        $data = $this->send('POST', $path);
+
+        return $data->success;
+    }
+
+    /**
+     * Compacting the on-disk database.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#compacting-the-on-disk-database
+     */
+    public function compact(): bool
+    {
+        $data = $this->send('POST', '/operations/db/compact');
+
+        return $data->success;
+    }
+
+    /**
+     * Triggers a follower node to initiate the raft voting process, which triggers leader re-election.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#re-elect-leader
+     */
+    public function reElectLeader(): bool
+    {
+        $data = $this->send('POST', '/operations/vote');
+
+        return $data->success;
+    }
+
+    /**
+     * Enable logging of requests that take over a defined threshold of time.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#toggle-slow-request-log
+     */
+    public function updateSlowRequestLog(int $ms): bool
+    {
+        $data = $this->send('POST', '/config', [
+            'log-slow-requests-time-ms' => $ms,
+        ]);
+
+        return $data->success;
+    }
+
+    /**
+     * Get current RAM, CPU, Disk & Network usage metrics.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#cluster-metrics
+     */
+    public function metrics(): Metric
+    {
+        $data = $this->send('GET', '/metrics.json');
+
+        return Metric::from($data);
+    }
+
+    /**
+     * Get stats about API endpoints.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#api-stats
+     */
+    public function stats(): Stat
+    {
+        $data = $this->send('GET', '/stats.json');
+
+        return Stat::from($data);
+    }
+
+    /**
+     * Get health information about a Typesense node.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#health
+     */
+    public function health(): bool
+    {
+        $data = $this->send('GET', '/health');
+
+        return $data->ok;
+    }
+}
diff --git a/src/Requests/Collection.php b/src/Requests/Collection.php
new file mode 100644
index 00000000..6b8b2d30
--- /dev/null
+++ b/src/Requests/Collection.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceAlreadyExistsException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Collection as CollectionObject;
+use Typesense\Objects\CollectionDroppedField;
+use Typesense\Objects\CollectionField;
+
+/**
+ * @phpstan-import-type TypesenseCollectionFieldLocale from CollectionField
+ * @phpstan-import-type TypesenseCollectionFieldType from CollectionField
+ *
+ * @phpstan-type CollectionFieldCreation array{
+ *     name: string,
+ *     type: TypesenseCollectionFieldType,
+ *     optional?: bool,
+ *     facet?: bool,
+ *     index?: bool,
+ *     infix?: bool,
+ *     sort?: bool,
+ *     locale?: TypesenseCollectionFieldLocale,
+ * }
+ */
+class Collection extends Request
+{
+    /**
+     * @param array{
+     *     name: string,
+     *     fields: non-empty-array<int, CollectionFieldCreation>,
+     *     enable_nested_fields?: bool,
+     *     token_separators?: array<int, string>,
+     *     symbols_to_index?: array<int, string>,
+     *     default_sorting_field?: string,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     * @throws ResourceAlreadyExistsException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#create-a-collection
+     */
+    public function create(array $payload): CollectionObject
+    {
+        $data = $this->send('POST', '/collections', $payload);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#cloning-a-collection-schema
+     */
+    public function clone(string $source, string $name): CollectionObject
+    {
+        $query = http_build_query([
+            'src_name' => $source,
+        ]);
+
+        $path = sprintf('/collections?%s', $query);
+
+        $payload = [
+            'name' => $name,
+        ];
+
+        $data = $this->send('POST', $path, $payload);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#retrieve-a-collection
+     */
+    public function retrieve(string $name): CollectionObject
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('GET', $path);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @return array<int, CollectionObject>
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#list-all-collections
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/collections', expectArray: true);
+
+        return array_map(
+            fn (stdClass $datum) => CollectionObject::from($datum),
+            $data,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#drop-a-collection
+     */
+    public function drop(string $name): CollectionObject
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @param  array<int, CollectionFieldCreation|array{name: string, drop: true}>  $fields
+     * @return array<int, CollectionField|CollectionDroppedField>
+     *
+     * @throws ResourceNotFoundException
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#update-or-alter-a-collection
+     */
+    public function update(string $name, array $fields): array
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('PATCH', $path, ['fields' => $fields]);
+
+        return array_map(function (stdClass $field) {
+            if (isset($field->drop)) {
+                return CollectionDroppedField::from($field);
+            }
+
+            return CollectionField::from($field);
+        }, $data->fields);
+    }
+}
diff --git a/src/Requests/Curation.php b/src/Requests/Curation.php
new file mode 100644
index 00000000..c28346b1
--- /dev/null
+++ b/src/Requests/Curation.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Curation as CurationObject;
+
+/**
+ * @phpstan-import-type CurationRule from CurationObject
+ * @phpstan-import-type CurationExclude from CurationObject
+ * @phpstan-import-type CurationInclude from CurationObject
+ */
+class Curation extends Request
+{
+    /**
+     * Create or update an override.
+     *
+     * @param array{
+     *     rule: CurationRule,
+     *     includes?: array<int, CurationInclude>,
+     *     excludes?: array<int, CurationExclude>,
+     *     filter_by?: string,
+     *     sort_by?: string,
+     *     replace_query?: string,
+     *     remove_matched_tokens?: bool,
+     *     filter_curated_hits?: bool,
+     *     effective_from_ts?: int,
+     *     effective_to_ts?: int,
+     *     stop_processing?: bool,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#create-or-update-an-override
+     */
+    public function upsert(string $collection, string $id, array $payload): CurationObject
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('PUT', $path, $payload);
+
+        return CurationObject::from($data);
+    }
+
+    /**
+     * Fetch an individual override associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#retrieve-an-override
+     */
+    public function retrieve(string $collection, string $id): CurationObject
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('GET', $path);
+
+        return CurationObject::from($data);
+    }
+
+    /**
+     * Listing all overrides associated with a given collection.
+     *
+     * @return array<int, CurationObject>
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#list-all-overrides
+     */
+    public function list(string $collection): array
+    {
+        $path = sprintf('/collections/%s/overrides', $collection);
+
+        $data = $this->send('GET', $path);
+
+        return array_map(
+            fn (stdClass $datum) => CurationObject::from($datum),
+            $data->overrides,
+        );
+    }
+
+    /**
+     * Deleting an override associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#delete-an-override
+     */
+    public function delete(string $collection, string $id): bool
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/src/Requests/Document.php b/src/Requests/Document.php
new file mode 100644
index 00000000..8ded46c6
--- /dev/null
+++ b/src/Requests/Document.php
@@ -0,0 +1,268 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Objects\Document as DocumentObject;
+use Typesense\Objects\GenericDocument;
+use Typesense\Objects\ImportedDocument;
+
+class Document extends Request
+{
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  array<string, mixed>  $payload
+     * @param  class-string<T>|null  $document
+     * @param  'create'|'upsert'  $action
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function index(
+        string $collection,
+        array $payload,
+        ?string $document = null,
+        string $action = 'create',
+    ): DocumentObject {
+        $query = http_build_query([
+            'action' => $action,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('POST', $path, $payload);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  array<string, mixed>  $payload
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function upsert(
+        string $collection,
+        array $payload,
+        ?string $document = null,
+    ): DocumentObject {
+        return $this->index($collection, $payload, $document, 'upsert');
+    }
+
+    /**
+     * @param  array<int, array<mixed>>  $payloads
+     * @param  'create'|'upsert'|'update'|'emplace'  $action
+     * @param  'coerce_or_reject'|'coerce_or_drop'|'drop'|'reject'  $dirty_values
+     * @return array<int, ImportedDocument>
+     */
+    public function import(
+        string $collection,
+        array $payloads,
+        string $action = 'create',
+        bool $return_id = false,
+        bool $return_doc = false,
+        string $dirty_values = 'coerce_or_reject',
+        int $batch_size = 40,
+    ): array {
+        $query = http_build_query([
+            'action' => $action,
+            'return_id' => $return_id ? 'true' : 'false',
+            'return_doc' => $return_doc ? 'true' : 'false',
+            'dirty_values' => $dirty_values,
+            'batch_size' => $batch_size,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents/import?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('POST', $path, $payloads, true, true);
+
+        return array_map(
+            fn (stdClass $datum) => ImportedDocument::from($datum),
+            $data,
+        );
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function retrieve(
+        string $collection,
+        string $id,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('GET', $path);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  array<string, mixed>  $payload
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function update(
+        string $collection,
+        string $id,
+        array $payload,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('PATCH', $path, $payload);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @param  array<mixed>  $payload
+     * @return int The number of total updated documents.
+     */
+    public function updateByQuery(
+        string $collection,
+        string $filter_by,
+        array $payload,
+    ): int {
+        $query = http_build_query([
+            'filter_by' => $filter_by,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('PATCH', $path, $payload);
+
+        return $data->num_updated;
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function delete(
+        string $collection,
+        string $id,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('DELETE', $path);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @return int The number of total deleted documents.
+     */
+    public function deleteByQuery(
+        string $collection,
+        string $filter_by,
+        int $batch_size = 100,
+    ): int {
+        $query = http_build_query([
+            'filter_by' => $filter_by,
+            'batch_size' => $batch_size,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->num_deleted;
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? array<int, T> : array<int, GenericDocument>)
+     */
+    public function export(
+        string $collection,
+        string $filter_by = '',
+        string $include_fields = '',
+        string $exclude_fields = '',
+        ?string $document = null,
+    ): array {
+        $query = http_build_query(
+            array_filter(
+                compact('filter_by', 'include_fields', 'exclude_fields'),
+            ),
+        );
+
+        $path = sprintf(
+            '/collections/%s/documents/export?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send(
+            'GET',
+            $path,
+            expectArray: true,
+            ndjson: true,
+        );
+
+        return array_map(
+            fn (stdClass $datum) => $this->toDocument($datum, $document),
+            $data,
+        );
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string<T>|null  $document
+     * @return ($document is class-string<T> ? T : GenericDocument)
+     */
+    public function toDocument(
+        stdClass $data,
+        ?string $document = null,
+    ): DocumentObject {
+        if (
+            $document === null ||
+            ! is_subclass_of($document, DocumentObject::class)
+        ) {
+            return GenericDocument::from($data);
+        }
+
+        return new $document($data);
+    }
+}
diff --git a/src/Requests/Key.php b/src/Requests/Key.php
new file mode 100644
index 00000000..537e3d6d
--- /dev/null
+++ b/src/Requests/Key.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Key as KeyObject;
+
+/**
+ * @phpstan-import-type KeyAction from KeyObject
+ */
+class Key extends Request
+{
+    /**
+     * @param array{
+     *     actions: array<int, KeyAction>,
+     *     collections: array<int, string>,
+     *     description: string,
+     *     value?: string,
+     *     expires_at?: int,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#create-an-api-key
+     */
+    public function create(array $payload): KeyObject
+    {
+        $data = $this->send('POST', '/keys', $payload);
+
+        return KeyObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#retrieve-an-api-key
+     */
+    public function retrieve(int $id): KeyObject
+    {
+        $path = sprintf('/keys/%d', $id);
+
+        $data = $this->send('GET', $path);
+
+        return KeyObject::from($data);
+    }
+
+    /**
+     * @return array<int, KeyObject>
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#list-all-keys
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/keys');
+
+        return array_map(
+            fn (stdClass $datum) => KeyObject::from($datum),
+            $data->keys,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#delete-api-key
+     */
+    public function delete(int $id): bool
+    {
+        $path = sprintf('/keys/%d', $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/src/Requests/Request.php b/src/Requests/Request.php
new file mode 100644
index 00000000..733b33b4
--- /dev/null
+++ b/src/Requests/Request.php
@@ -0,0 +1,127 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceAlreadyExistsException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Exceptions\Client\UnauthorizedException;
+use Typesense\Exceptions\Client\UnprocessableEntityException;
+use Typesense\Exceptions\MalformedResponsePayloadException;
+use Typesense\Exceptions\Server\ServiceUnavailableException;
+use Typesense\Exceptions\UnknownHttpException;
+use Typesense\Http;
+
+abstract class Request
+{
+    /**
+     * Constructor.
+     */
+    public function __construct(
+        public Http $http,
+    ) {
+        //
+    }
+
+    /**
+     * @param  'GET'|'POST'|'PATCH'|'PUT'|'DELETE'  $method
+     * @param  ($ndjson is true ? array<int, array<string, mixed>> : array<string, mixed>)  $body
+     * @return ($expectArray is false ? stdClass : array<int, stdClass>)
+     *
+     * @throws UnauthorizedException
+     */
+    public function send(
+        string $method,
+        string $path,
+        array $body = [],
+        bool $expectArray = false,
+        bool $ndjson = false,
+    ): stdClass|array {
+        if (! $ndjson) {
+            $form = json_encode($body) ?: '';
+        } else {
+            $form = array_map(
+                fn (array $payload) => json_encode($payload),
+                $body,
+            );
+
+            $form = implode(PHP_EOL, array_values(array_filter($form)));
+        }
+
+        $response = $this->http->request($method, $path, $form);
+
+        $contents = $response->getBody()->getContents();
+
+        $context = json_decode($contents, false);
+
+        $status = $response->getStatusCode();
+
+        if (! ($status >= 200 && $status < 300)) {
+            if (! ($context instanceof stdClass) || ! is_string($context->message)) {
+                throw new MalformedResponsePayloadException($contents);
+            }
+
+            $this->toException($status, $context->message);
+        }
+
+        if ($ndjson) {
+            return array_map(
+                function (string $data) {
+                    $result = json_decode($data, false);
+
+                    return $result instanceof stdClass ? $result : new stdClass();
+                },
+                explode(PHP_EOL, $contents),
+            );
+        }
+
+        if ($expectArray) {
+            if (! is_array($context)) {
+                throw new MalformedResponsePayloadException($contents);
+            }
+
+            return $context;
+        }
+
+        if (! ($context instanceof stdClass)) {
+            throw new MalformedResponsePayloadException($contents);
+        }
+
+        return $context;
+    }
+
+    /**
+     * Throw exception by the status code.
+     */
+    public function toException(int $status, string $message): void
+    {
+        if ($status === 400) {
+            throw new InvalidPayloadException($message);
+        }
+
+        if ($status === 401) {
+            throw new UnauthorizedException('Missing API key or the API key is invalid.');
+        }
+
+        if ($status === 404) {
+            throw new ResourceNotFoundException($message);
+        }
+
+        if ($status === 409) {
+            throw new ResourceAlreadyExistsException($message);
+        }
+
+        if ($status === 422) {
+            throw new UnprocessableEntityException($message);
+        }
+
+        if ($status === 503) {
+            throw new ServiceUnavailableException();
+        }
+
+        throw new UnknownHttpException();
+    }
+}
diff --git a/src/Requests/Synonym.php b/src/Requests/Synonym.php
new file mode 100644
index 00000000..9733e42b
--- /dev/null
+++ b/src/Requests/Synonym.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Requests;
+
+use stdClass;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+use Typesense\Objects\Synonym as SynonymObject;
+
+class Synonym extends Request
+{
+    /**
+     * Create or update a synonym.
+     *
+     * @param array{
+     *     synonyms: array<int, string>,
+     *     root?: string,
+     *     locale?: string,
+     *     symbols_to_index?: array<int, string>,
+     * } $payload
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#create-or-update-a-synonym
+     */
+    public function upsert(string $collection, string $id, array $payload): SynonymObject
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('PUT', $path, $payload);
+
+        return SynonymObject::from($data);
+    }
+
+    /**
+     * Retrieve a single synonym.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#retrieve-a-synonym
+     */
+    public function retrieve(string $collection, string $id): SynonymObject
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('GET', $path);
+
+        return SynonymObject::from($data);
+    }
+
+    /**
+     * List all synonyms associated with a given collection.
+     *
+     * @return array<int, SynonymObject>
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#list-all-synonyms
+     */
+    public function list(string $collection): array
+    {
+        $path = sprintf('/collections/%s/synonyms', $collection);
+
+        $data = $this->send('GET', $path);
+
+        return array_map(
+            fn (stdClass $datum) => SynonymObject::from($datum),
+            $data->synonyms,
+        );
+    }
+
+    /**
+     * Delete a synonym associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#delete-a-synonym
+     */
+    public function delete(string $collection, string $id): bool
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/src/Synonym.php b/src/Synonym.php
deleted file mode 100644
index 5ce46c15..00000000
--- a/src/Synonym.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class synonym
- *
- * @package \Typesense
- */
-class Synonym
-{
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var string
-     */
-    private string $synonymId;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * synonym constructor.
-     *
-     * @param string $collectionName
-     * @param string $synonymId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, string $synonymId, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->synonymId      = $synonymId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endPointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            synonyms::RESOURCE_PATH,
-            $this->synonymId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Synonyms.php b/src/Synonyms.php
deleted file mode 100644
index f48f2144..00000000
--- a/src/Synonyms.php
+++ /dev/null
@@ -1,117 +0,0 @@
-<?php
-
-namespace Typesense;
-
-use Http\Client\Exception as HttpClientException;
-use Typesense\Exceptions\TypesenseClientError;
-
-/**
- * Class Synonyms
- *
- * @package \Typesense
- */
-class Synonyms implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = 'synonyms';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var array
-     */
-    private array $synonyms = [];
-
-    /**
-     * Synonyms constructor.
-     *
-     * @param string $collectionName
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $synonymId
-     *
-     * @return string
-     */
-    public function endPointPath(string $synonymId = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $synonymId
-        );
-    }
-
-    /**
-     * @param string $synonymId
-     * @param array $config
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $synonymId, array $config): array
-    {
-        return $this->apiCall->put($this->endPointPath($synonymId), $config);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($synonymId): bool
-    {
-        return isset($this->synonyms[$synonymId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($synonymId): Synonym
-    {
-        if (!isset($this->synonyms[$synonymId])) {
-            $this->synonyms[$synonymId] = new Synonym($this->collectionName, $synonymId, $this->apiCall);
-        }
-
-        return $this->synonyms[$synonymId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($synonymId, $value): void
-    {
-        $this->synonyms[$synonymId] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($synonymId): void
-    {
-        unset($this->synonyms[$synonymId]);
-    }
-}
diff --git a/src/Typesense.php b/src/Typesense.php
new file mode 100644
index 00000000..0ebe2cd7
--- /dev/null
+++ b/src/Typesense.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense;
+
+use Psr\Http\Client\ClientInterface;
+use Typesense\Requests\Alias;
+use Typesense\Requests\Analytic;
+use Typesense\Requests\Cluster;
+use Typesense\Requests\Collection;
+use Typesense\Requests\Curation;
+use Typesense\Requests\Document;
+use Typesense\Requests\Key;
+use Typesense\Requests\Synonym;
+
+/**
+ * @phpstan-type TypesenseConfiguration array{
+ *     url: string,
+ *     apiKey: string,
+ *     http?: ClientInterface|null,
+ * }
+ */
+class Typesense
+{
+    public readonly Http $http;
+
+    public readonly Collection $collection;
+
+    public readonly Document $document;
+
+    public readonly Analytic $analytic;
+
+    public readonly Key $key;
+
+    public readonly Curation $curation;
+
+    public readonly Alias $alias;
+
+    public readonly Synonym $synonym;
+
+    public readonly Cluster $cluster;
+
+    /**
+     * @param  TypesenseConfiguration  $config
+     */
+    public function __construct(array $config)
+    {
+        $this->http = new Http($config);
+
+        $this->collection = new Collection($this->http);
+
+        $this->document = new Document($this->http);
+
+        $this->analytic = new Analytic($this->http);
+
+        $this->key = new Key($this->http);
+
+        $this->curation = new Curation($this->http);
+
+        $this->alias = new Alias($this->http);
+
+        $this->synonym = new Synonym($this->http);
+
+        $this->cluster = new Cluster($this->http);
+    }
+
+    public function setUrl(string $url): static
+    {
+        $this->http->config['url'] = $url;
+
+        return $this;
+    }
+
+    public function setApiKey(string $key): static
+    {
+        $this->http->config['apiKey'] = $key;
+
+        return $this;
+    }
+
+    public function setHttp(ClientInterface $http): static
+    {
+        $this->http->config['http'] = $http;
+
+        return $this;
+    }
+}
diff --git a/tests/Objects/TestObject.php b/tests/Objects/TestObject.php
new file mode 100644
index 00000000..f7020abe
--- /dev/null
+++ b/tests/Objects/TestObject.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Objects;
+
+use Typesense\Objects\Document;
+
+final class TestObject extends Document
+{
+    public string $name;
+
+    public string $description;
+}
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 00000000..e495ea5e
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,7 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests;
+
+uses(TestCase::class)->in('Unit');
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..80d36c1f
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests;
+
+use PHPUnit\Framework\TestCase as BaseTestCase;
+use Typesense\Typesense;
+
+use function Pest\Faker\fake;
+
+abstract class TestCase extends BaseTestCase
+{
+    public Typesense $typesense;
+
+    public string $collectionName = 'testing';
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->typesense = new Typesense([
+            'url' => 'http://localhost:8108',
+            'apiKey' => 'testing',
+        ]);
+
+        $this->createTestingCollection();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function tearDown(): void
+    {
+        $this->typesense->collection->drop($this->collectionName);
+
+        parent::tearDown();
+    }
+
+    /**
+     * Create testing collection.
+     */
+    protected function createTestingCollection(): void
+    {
+        $this->typesense->collection->create([
+            'name' => $this->collectionName,
+            'fields' => [
+                [
+                    'name' => 'name',
+                    'type' => 'string',
+                ],
+                [
+                    'name' => 'description',
+                    'type' => 'string',
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * Generate random slug.
+     */
+    public function slug(): string
+    {
+        return fake()->unique()->slug(variableNbWords: false);
+    }
+}
diff --git a/tests/Unit/AliasTest.php b/tests/Unit/AliasTest.php
new file mode 100644
index 00000000..eafe2f37
--- /dev/null
+++ b/tests/Unit/AliasTest.php
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can create an alias to an existing collection', function () {
+    $alias = $this->typesense->alias->upsert(
+        $name = $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    expect($alias->name)->toBe($name);
+
+    expect($alias->collection_name)->toBe($this->collectionName);
+});
+
+test('it can create an alias to a non-existent collection', function () {
+    $alias = $this->typesense->alias->upsert(
+        $name = $this->slug(),
+        [
+            'collection_name' => $collection = $this->slug(),
+        ],
+    );
+
+    expect($alias->name)->toBe($name);
+
+    expect($alias->collection_name)->toBe($collection);
+});
+
+test('it can not create an alias without collection name', function () {
+    $this->typesense->alias->upsert(
+        $this->slug(),
+        [],
+    );
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing alias', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $nAlias = $this->typesense->alias->retrieve($alias->name);
+
+    expect($nAlias->name)->toBe($alias->name);
+});
+
+test('it can not retrieve a non-existent alias', function () {
+    $this->typesense->alias->retrieve(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all aliases', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $aliases = $this->typesense->alias->list();
+
+    $names = array_column($aliases, 'name');
+
+    expect($alias->name)->toBeIn($names);
+});
+
+test('it can delete an existing alias', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $deleted = $this->typesense->alias->delete($alias->name);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent alias', function () {
+    $this->typesense->alias->delete(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
diff --git a/tests/Unit/AnalyticTest.php b/tests/Unit/AnalyticTest.php
new file mode 100644
index 00000000..8fd67ff7
--- /dev/null
+++ b/tests/Unit/AnalyticTest.php
@@ -0,0 +1,125 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceAlreadyExistsException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can use setup to create aggregate collection', function () {
+    $created = $this->typesense->analytic->setup(
+        $this->slug(),
+    );
+
+    expect($created)->toBeTrue();
+});
+
+test('it can not use setup to create aggregate collection with existing collection name', function () {
+    $this->typesense->analytic->setup(
+        $name = $this->slug(),
+    );
+
+    $this->typesense->analytic->setup(
+        $name,
+    );
+})->throws(ResourceAlreadyExistsException::class);
+
+test('it can create an analytic rule', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $rule = $this->typesense->analytic->create([
+        'name' => $name = $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    expect($rule->name)->toBe($name);
+});
+
+test('it can not create an analytic rule with invalid type', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => $this->slug(),
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can list all analytic rules', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $analytic = $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    $analytics = $this->typesense->analytic->list();
+
+    $names = array_column($analytics, 'name');
+
+    expect($analytic->name)->toBeIn($names);
+});
+
+test('it can delete an existing analytic rule', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $analytic = $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    $deleted = $this->typesense->analytic->delete($analytic->name);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent analytic rule', function () {
+    $this->typesense->analytic->delete(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
diff --git a/tests/Unit/ArchitectureTest.php b/tests/Unit/ArchitectureTest.php
new file mode 100644
index 00000000..d17b62a4
--- /dev/null
+++ b/tests/Unit/ArchitectureTest.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\TypesenseException;
+
+test('there are no debugging statements remaining in the code')
+    ->expect(['dd', 'dump', 'ray', 'var_dump', 'echo', 'print_r'])
+    ->not()
+    ->toBeUsed();
+
+test('strict typing must be enforced in the code')
+    ->expect('Typesense')
+    ->toUseStrictTypes();
+
+test('the code should not utilize the "final" keyword')
+    ->expect('Typesense')
+    ->not()
+    ->toBeFinal();
+
+test('all exception classes should extend "TypesenseException"')
+    ->expect('Typesense\Exceptions')
+    ->classes()
+    ->toExtend(TypesenseException::class);
diff --git a/tests/Unit/AuthenticationTest.php b/tests/Unit/AuthenticationTest.php
new file mode 100644
index 00000000..a8d23948
--- /dev/null
+++ b/tests/Unit/AuthenticationTest.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\UnauthorizedException;
+
+test('it must set a valid api key when calling API', function () {
+    $origin = $this->typesense->http->config['apiKey'];
+
+    $this->typesense->setApiKey($this->slug());
+
+    expect(fn () => $this->typesense->collection->list())
+        ->toThrow(UnauthorizedException::class);
+
+    $this->typesense->setApiKey($origin);
+});
diff --git a/tests/Unit/ClusterTest.php b/tests/Unit/ClusterTest.php
new file mode 100644
index 00000000..5b12c39d
--- /dev/null
+++ b/tests/Unit/ClusterTest.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+test('it can create a snapshot', function () {
+    $path = sprintf('/tmp/%s', $this->slug());
+
+    $success = $this->typesense->cluster->snapshot(
+        $path,
+    );
+
+    expect($success)->toBeTrue();
+});
+
+test('it can compact the database', function () {
+    $success = $this->typesense->cluster->compact();
+
+    expect($success)->toBeTrue();
+});
+
+test('it can update slow request log', function () {
+    $success = $this->typesense->cluster->updateSlowRequestLog(100);
+
+    expect($success)->toBeTrue();
+});
+
+test('it can get metrics', function () {
+    $metric = $this->typesense->cluster->metrics();
+
+    expect($metric->typesense_memory_retained_bytes)->toBeGreaterThanOrEqual(0);
+});
+
+test('it can get stats', function () {
+    $stats = $this->typesense->cluster->stats();
+
+    expect($stats->delete_latency_ms)->toBeGreaterThanOrEqual(0);
+});
+
+test('it can get health', function () {
+    $health = $this->typesense->cluster->health();
+
+    expect($health)->toBeTrue();
+});
diff --git a/tests/Unit/CollectionTest.php b/tests/Unit/CollectionTest.php
new file mode 100644
index 00000000..e1c12599
--- /dev/null
+++ b/tests/Unit/CollectionTest.php
@@ -0,0 +1,223 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceAlreadyExistsException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can create collection', function () {
+    $collectionName = $this->slug();
+
+    $fieldName = $this->slug();
+
+    $fieldType = 'string';
+
+    $collection = $this->typesense->collection->create([
+        'name' => $collectionName,
+        'fields' => [
+            [
+                'name' => $fieldName,
+                'type' => $fieldType,
+            ],
+        ],
+    ]);
+
+    expect($collection->created_at)->toBeInt();
+
+    expect($collection->default_sorting_field)->toBeEmpty();
+
+    expect($collection->enable_nested_fields)->toBeFalse();
+
+    expect($collection->name)->toBe($collectionName);
+
+    expect($collection->num_documents)->toBe(0);
+
+    expect($collection->symbols_to_index)->toBeEmpty();
+
+    expect($collection->token_separators)->toBeEmpty();
+
+    expect($collection->fields)->toHaveCount(1);
+
+    $field = $collection->fields[0];
+
+    expect($field->facet)->toBeFalse();
+
+    expect($field->index)->toBeTrue();
+
+    expect($field->infix)->toBeFalse();
+
+    expect($field->locale)->toBeEmpty();
+
+    expect($field->name)->toBe($fieldName);
+
+    expect($field->optional)->toBeFalse();
+
+    expect($field->sort)->toBeFalse();
+
+    expect($field->type)->toBe($fieldType);
+});
+
+test('it can not create collection if the name already exists', function () {
+    $this->typesense->collection->create([
+        'name' => $this->collectionName,
+        'fields' => [
+            [
+                'name' => $this->slug(),
+                'type' => 'string',
+            ],
+        ],
+    ]);
+})->throws(ResourceAlreadyExistsException::class);
+
+test('it can not create collection if the name is empty', function () {
+    $this->typesense->collection->create([
+        'name' => '',
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create collection if the fields is empty', function () {
+    $this->typesense->collection->create([
+        'name' => $this->slug(),
+        'fields' => [],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create collection if the fields are invalid', function () {
+    $this->typesense->collection->create([
+        'name' => $this->slug(),
+        'fields' => [
+            [
+                'name' => '',
+                'type' => 'string',
+            ],
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can clone collection', function () {
+    $target = $this->slug();
+
+    $collection = $this->typesense
+        ->collection
+        ->clone($this->collectionName, $target);
+
+    expect($collection->name)->toBe($target);
+});
+
+test('it can not clone collection if source collection does not exist', function () {
+    $this->typesense
+        ->collection
+        ->clone($this->slug(), $this->slug());
+})->throws(InvalidPayloadException::class);
+
+test('it can not clone collection if target collection already exists', function () {
+    $this->typesense
+        ->collection
+        ->clone($this->collectionName, $this->collectionName);
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve collection', function () {
+    $collection = $this->typesense
+        ->collection
+        ->retrieve($this->collectionName);
+
+    expect($collection->name)->toBe($this->collectionName);
+});
+
+test('it can not retrieve a non-existent collection', function () {
+    $this->typesense
+        ->collection
+        ->retrieve($this->slug());
+})->throws(ResourceNotFoundException::class);
+
+test('it can list collection', function () {
+    $collections = $this->typesense
+        ->collection
+        ->list();
+
+    expect($collections)
+        ->toBeArray()
+        ->toBeGreaterThanOrEqual(1);
+});
+
+test('it can drop collection', function () {
+    $name = $this->slug();
+
+    $this->typesense->collection->create([
+        'name' => $name,
+        'fields' => [
+            [
+                'name' => $this->slug(),
+                'type' => 'string',
+            ],
+        ],
+    ]);
+
+    $collection = $this->typesense->collection->drop($name);
+
+    expect($collection->name)->toBe($name);
+});
+
+test('it can not drop a non-existent collection', function () {
+    $this->typesense->collection->drop($this->slug());
+})->throws(ResourceNotFoundException::class);
+
+test('it can update collection', function () {
+    $name = $this->slug();
+
+    $fields = $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => $name,
+            'type' => 'int32',
+        ],
+    ]);
+
+    expect($fields)->toHaveCount(1);
+
+    expect($fields[0]->name)->toBe($name);
+
+    expect($fields[0]->type)->toBe('int32');
+
+    $fields = $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => $name,
+            'drop' => true,
+        ],
+    ]);
+
+    expect($fields)->toHaveCount(1);
+
+    expect($fields[0]->name)->toBe($name);
+
+    expect($fields[0]->drop)->toBeTrue();
+});
+
+test('it can not update a non-existent collection', function () {
+    $this->typesense->collection->update($this->slug(), [
+        [
+            'name' => $this->slug(),
+            'type' => 'int32',
+        ],
+    ]);
+})->throws(ResourceNotFoundException::class);
+
+test('it can not update an existing collection field', function () {
+    $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => 'name',
+            'type' => 'int32',
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not set drop to "false" for an existing collection field', function () {
+    $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => 'name',
+            'drop' => false,
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
diff --git a/tests/Unit/CurationTest.php b/tests/Unit/CurationTest.php
new file mode 100644
index 00000000..917e4371
--- /dev/null
+++ b/tests/Unit/CurationTest.php
@@ -0,0 +1,113 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can create a curation', function () {
+    $curation = $this->typesense->curation->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    expect($curation->id)->toBe($id);
+});
+
+test('it can not create a curation without required fields', function () {
+    $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+        ],
+    );
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing curation', function () {
+    $this->typesense->curation->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $curation = $this->typesense->curation->retrieve(
+        $this->collectionName,
+        $id,
+    );
+
+    expect($curation->id)->toBe($id);
+});
+
+test('it can not retrieve a non--existent curation', function () {
+    $this->typesense->curation->retrieve(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all curations', function () {
+    $curation = $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $curations = $this->typesense->curation->list($this->collectionName);
+
+    $ids = array_column($curations, 'id');
+
+    expect($curation->id)->toBeIn($ids);
+});
+
+test('it can delete an existing curation', function () {
+    $curation = $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $deleted = $this->typesense->curation->delete(
+        $this->collectionName,
+        $curation->id,
+    );
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent curation', function () {
+    $this->typesense->curation->delete(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
diff --git a/tests/Unit/DocumentTest.php b/tests/Unit/DocumentTest.php
new file mode 100644
index 00000000..ea9a1f7b
--- /dev/null
+++ b/tests/Unit/DocumentTest.php
@@ -0,0 +1,276 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Tests\Objects\TestObject;
+
+test('it can index document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can upsert document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->upsert(
+        'testing',
+        [
+            'id' => $document->id,
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can import document', function () {
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        TestObject::class,
+    );
+
+    $documents = [
+        [
+            'id' => $document->id,
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $results = $this->typesense->document->import(
+        'testing',
+        $documents,
+        return_id: true,
+        return_doc: true,
+    );
+
+    expect($results)->toHaveCount(count($documents));
+
+    expect($results[0]->success)->toBeFalse();
+
+    expect($results[1]->success)->toBeTrue();
+
+    expect($results[2]->success)->toBeTrue();
+});
+
+test('it can retrieve document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    $document = $this->typesense->document->retrieve('testing', $document->id);
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can update document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    $description = $this->slug();
+
+    expect($document->description)->not()->toBe($description);
+
+    $document = $this->typesense->document->update(
+        'testing',
+        $document->id,
+        [
+            'description' => $description,
+        ],
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can update documents by query', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '3',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $description = $this->slug();
+
+    $updated = $this->typesense->document->updateByQuery(
+        'testing',
+        'id:[2,3]',
+        [
+            'description' => $description,
+        ],
+    );
+
+    expect($updated)->toBe(2);
+});
+
+test('it can delete document', function () {
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        TestObject::class,
+    );
+
+    $id = $document->id;
+
+    $document = $this->typesense->document->delete('testing', $id);
+
+    expect($document->id)->toBe($id);
+});
+
+test('it can delete documents by query', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '3',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $deleted = $this->typesense->document->deleteByQuery(
+        'testing',
+        'id:[2,3]',
+    );
+
+    expect($deleted)->toBe(2);
+});
+
+test('it can export documents', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $exports = $this->typesense->document->export(
+        'testing',
+        document: TestObject::class,
+    );
+
+    expect($exports)->toHaveCount(count($documents));
+});
diff --git a/tests/Unit/HttpClientTest.php b/tests/Unit/HttpClientTest.php
new file mode 100644
index 00000000..27b6401e
--- /dev/null
+++ b/tests/Unit/HttpClientTest.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use GuzzleHttp\Client;
+use Symfony\Component\HttpClient\Psr18Client;
+use Typesense\Objects\Collection;
+
+test('it can use symfony http client', function () {
+    $this->typesense->setHttp(
+        new Psr18Client(),
+    );
+
+    $collection = $this->typesense->collection->retrieve(
+        $this->collectionName,
+    );
+
+    expect($collection)->toBeInstanceOf(Collection::class);
+});
+
+test('it can use guzzle http client', function () {
+    $this->typesense->setHttp(
+        new Client(),
+    );
+
+    $collection = $this->typesense->collection->retrieve(
+        $this->collectionName,
+    );
+
+    expect($collection)->toBeInstanceOf(Collection::class);
+});
diff --git a/tests/Unit/KeyTest.php b/tests/Unit/KeyTest.php
new file mode 100644
index 00000000..daa2a65e
--- /dev/null
+++ b/tests/Unit/KeyTest.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\InvalidPayloadException;
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can create an api key', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['collections:*'],
+        'collections' => ['*'],
+        'description' => $description = $this->slug(),
+        'expires_at' => $expires_at = time() + 100,
+    ]);
+
+    expect($key->actions)->toBe(['collections:*']);
+
+    expect($key->collections)->toBe(['*']);
+
+    expect($key->description)->toBe($description);
+
+    expect($key->expires_at)->toBe($expires_at);
+
+    expect($key->id)->toBeInt();
+
+    expect($key->value)->toBeString();
+
+    expect($key->value_prefix)->toBeNull();
+});
+
+test('it can not create an api key without actions', function () {
+    $this->typesense->key->create([
+        'actions' => [],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create an api key without collections', function () {
+    $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => [],
+        'description' => $this->slug(),
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can create an api key without description', function () {
+    $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing key', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $nKey = $this->typesense->key->retrieve($key->id);
+
+    expect($nKey->id)->toBe($key->id);
+
+    expect($nKey->expires_at)->toBe($key->expires_at);
+});
+
+test('it can not retrieve a non-existent key', function () {
+    $this->typesense->key->retrieve(
+        mt_rand(1000000000, 2147483647),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all api keys', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $keys = $this->typesense->key->list();
+
+    $ids = array_column($keys, 'id');
+
+    expect($key->id)->toBeIn($ids);
+});
+
+test('it can delete an existing key', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $deleted = $this->typesense->key->delete($key->id);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent key', function () {
+    $this->typesense->key->delete(
+        mt_rand(1000000000, 2147483647),
+    );
+})->throws(ResourceNotFoundException::class);
diff --git a/tests/Unit/MiscellaneousTest.php b/tests/Unit/MiscellaneousTest.php
new file mode 100644
index 00000000..a54b8d3f
--- /dev/null
+++ b/tests/Unit/MiscellaneousTest.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+test('it will throw error', function () {
+    $origin = $this->typesense->http->config['url'];
+
+    $this->typesense->setUrl('http://0.0.0.0:1');
+
+    expect(
+        fn () => $this->typesense->collection->retrieve('testing'),
+    )
+        ->toThrow('0.0.0.0');
+
+    $this->typesense->setUrl($origin);
+});
diff --git a/tests/Unit/ObjectCreationTest.php b/tests/Unit/ObjectCreationTest.php
new file mode 100644
index 00000000..fd7088dd
--- /dev/null
+++ b/tests/Unit/ObjectCreationTest.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use stdClass;
+use Typesense\Tests\Objects\TestObject;
+
+test('it will automatically assign properties', function () {
+    $data = new stdClass();
+
+    $data->name = 'Hello';
+
+    $data->description = 'World';
+
+    $object = TestObject::from($data);
+
+    expect($object->name)->toBe($data->name);
+
+    expect($object->description)->toBe($data->description);
+});
diff --git a/tests/Unit/SynonymTest.php b/tests/Unit/SynonymTest.php
new file mode 100644
index 00000000..b7637a80
--- /dev/null
+++ b/tests/Unit/SynonymTest.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Typesense\Tests\Unit;
+
+use Typesense\Exceptions\Client\ResourceNotFoundException;
+
+test('it can create a multi-way synonym', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'synonyms' => $synonyms = ['apple', 'banana', 'car'],
+        ],
+    );
+
+    expect($synonym->id)->toBe($id);
+
+    expect($synonym->synonyms)->toBe($synonyms);
+
+    expect($synonym->root)->toBe('');
+});
+
+test('it can create a one-way synonym', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'root' => $root = 'dog',
+            'synonyms' => $synonyms = ['apple', 'banana', 'car'],
+        ],
+    );
+
+    expect($synonym->id)->toBe($id);
+
+    expect($synonym->synonyms)->toBe($synonyms);
+
+    expect($synonym->root)->toBe($root);
+});
+
+test('it can retrieve an existing synonym', function () {
+    $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $synonym = $this->typesense->synonym->retrieve(
+        $this->collectionName,
+        $id,
+    );
+
+    expect($synonym->id)->toBe($id);
+});
+
+test('it can not retrieve a non-existent synonym', function () {
+    $this->typesense->synonym->retrieve(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all synonyms', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $synonyms = $this->typesense->synonym->list($this->collectionName);
+
+    $ids = array_column($synonyms, 'id');
+
+    expect($synonym->id)->toBeIn($ids);
+});
+
+test('it can delete an existing synonym', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $deleted = $this->typesense->synonym->delete(
+        $this->collectionName,
+        $synonym->id,
+    );
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent synonym', function () {
+    $this->typesense->synonym->delete(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);